Date: Wed, 1 Oct 2025 17:44:33 +0930
On Wed, 1 Oct 2025 at 02:51, Rhidian De Wit via Std-Proposals <
std-proposals_at_[hidden]> wrote:
> Hi all,
>
> A problem I regularly face at work is asynchronous lambdas, specifically
> the fact that the closure of an asynchronous lambda does not necessarily
> stay in scope after a suspension point, meaning that even if the captured
> variables remain in-scope, the capture group itself goes out-of-scope,
> causing all captured values in the lambda to become garbage.
>
> This is of course mentioned in the C++ core guidelines:
> https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#cp51-do-not-use-capturing-lambdas-that-are-coroutines
>
> However, I find myself wondering: why?
>
There are cases where you want to avoid taking copies of the lambda and its
captures.
This might be for performance reasons - e.g. you know that the lambda will
still be alive
within the lifetime of the coroutine-frame that is referencing it - or it
might be because the
lambda has some state in it that will be updated during execution of the
coroutine and that
you want the mutations to be visible to a subsequent invocation.
There are also cases where coroutines are used for synchronous functions,
like std::optional
or std::expected-returning functions, where there is no chance of the
coroutine ending up
with a dangling reference to the lambda.
Unconditionally copying/moving the lambda state into the coroutine frame
would have
penalised these situations.
> Why does the capture group out-of-scope?
> From cppreference
> <https://en.cppreference.com/w/cpp/language/coroutines.html>:
>
> - *the coroutine state, which is internal, dynamically-allocated
> storage (unless the allocation is optimized out), object that contains *
>
>
> - * the promise object *
> - * the parameters (all copied by value) *
> - * some representation of the current suspension point, so that a
> resume knows where to continue, and a destroy knows what local variables
> were in scope *
> - * local variables and temporaries whose lifetime spans the current
> suspension point. *
>
> *When a coroutine begins execution, it performs the following: *
>
> - *allocates
> <https://en.cppreference.com/w/cpp/language/coroutines.html#Dynamic_allocation>
> the coroutine state object using operator new
> <https://en.cppreference.com/w/cpp/memory/new/operator_new.html>. *
> - * copies all function parameters to the coroutine state: by-value
> parameters are moved or copied, by-reference parameters remain references
> (thus, may become dangling, if the coroutine is resumed after the lifetime
> of referred object ends — see below for examples). *
>
> Could the lambda capture group not be allocated on the heap alongside all
> other heap allocated memory? This would make asynchronous lambdas possible
> and safe-to-use, which would be great, instead of having to always rely on
> actual functions.
>
> The reason that this is a problem for us is because of backwards
> compatability: We have a lot of old asynchronous code predating coroutines,
> and during conversions to coroutines, such details are easily forgotten,
> especially by people who aren't very used to coroutines and their quirks.
>
> Is there any ongoing proposal to address this problem?
>
The solution is to add an explicit object parameter in the lambda which
passes the lambda by-value to the coroutine.
i.e. write
task t = [&](this auto) -> task {
// ...
co_return;
}();
This will cause the lambda itself to be captured by value as a parameter in
the coroutine-frame
and references to captured variables within the lambda will implicitly
refer to the captures stored
in the coroutine-frame's copy of the lambda.
For example, see https://godbolt.org/z/cdfKf9W41
The core-guideline
<https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#cp51-do-not-use-capturing-lambdas-that-are-coroutines>
on
this probably needs to be updated to say "use explicit object parameter on
coroutine lambdas
unless you otherwise ensure that the lambda itself stays alive until the
coroutine completes" rather than a blanket
rule saying "don't use capturing coroutine lambdas" now that we have access
to explicit object parameters.
Cheers,
Lewis.
std-proposals_at_[hidden]> wrote:
> Hi all,
>
> A problem I regularly face at work is asynchronous lambdas, specifically
> the fact that the closure of an asynchronous lambda does not necessarily
> stay in scope after a suspension point, meaning that even if the captured
> variables remain in-scope, the capture group itself goes out-of-scope,
> causing all captured values in the lambda to become garbage.
>
> This is of course mentioned in the C++ core guidelines:
> https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#cp51-do-not-use-capturing-lambdas-that-are-coroutines
>
> However, I find myself wondering: why?
>
There are cases where you want to avoid taking copies of the lambda and its
captures.
This might be for performance reasons - e.g. you know that the lambda will
still be alive
within the lifetime of the coroutine-frame that is referencing it - or it
might be because the
lambda has some state in it that will be updated during execution of the
coroutine and that
you want the mutations to be visible to a subsequent invocation.
There are also cases where coroutines are used for synchronous functions,
like std::optional
or std::expected-returning functions, where there is no chance of the
coroutine ending up
with a dangling reference to the lambda.
Unconditionally copying/moving the lambda state into the coroutine frame
would have
penalised these situations.
> Why does the capture group out-of-scope?
> From cppreference
> <https://en.cppreference.com/w/cpp/language/coroutines.html>:
>
> - *the coroutine state, which is internal, dynamically-allocated
> storage (unless the allocation is optimized out), object that contains *
>
>
> - * the promise object *
> - * the parameters (all copied by value) *
> - * some representation of the current suspension point, so that a
> resume knows where to continue, and a destroy knows what local variables
> were in scope *
> - * local variables and temporaries whose lifetime spans the current
> suspension point. *
>
> *When a coroutine begins execution, it performs the following: *
>
> - *allocates
> <https://en.cppreference.com/w/cpp/language/coroutines.html#Dynamic_allocation>
> the coroutine state object using operator new
> <https://en.cppreference.com/w/cpp/memory/new/operator_new.html>. *
> - * copies all function parameters to the coroutine state: by-value
> parameters are moved or copied, by-reference parameters remain references
> (thus, may become dangling, if the coroutine is resumed after the lifetime
> of referred object ends — see below for examples). *
>
> Could the lambda capture group not be allocated on the heap alongside all
> other heap allocated memory? This would make asynchronous lambdas possible
> and safe-to-use, which would be great, instead of having to always rely on
> actual functions.
>
> The reason that this is a problem for us is because of backwards
> compatability: We have a lot of old asynchronous code predating coroutines,
> and during conversions to coroutines, such details are easily forgotten,
> especially by people who aren't very used to coroutines and their quirks.
>
> Is there any ongoing proposal to address this problem?
>
The solution is to add an explicit object parameter in the lambda which
passes the lambda by-value to the coroutine.
i.e. write
task t = [&](this auto) -> task {
// ...
co_return;
}();
This will cause the lambda itself to be captured by value as a parameter in
the coroutine-frame
and references to captured variables within the lambda will implicitly
refer to the captures stored
in the coroutine-frame's copy of the lambda.
For example, see https://godbolt.org/z/cdfKf9W41
The core-guideline
<https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#cp51-do-not-use-capturing-lambdas-that-are-coroutines>
on
this probably needs to be updated to say "use explicit object parameter on
coroutine lambdas
unless you otherwise ensure that the lambda itself stays alive until the
coroutine completes" rather than a blanket
rule saying "don't use capturing coroutine lambdas" now that we have access
to explicit object parameters.
Cheers,
Lewis.
Received on 2025-10-01 08:14:48