-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Implement the feature described in this paper:
Document number: PXXXXR0
Date: 2026-01-30
Reply-to: Vinnie Falco <vinnie.falco@gmail.com>
Audience: EWG, SG21
Lambda Coroutine Capture Warning Attribute
Abstract
Lambda coroutines that capture variables have a subtle but critical flaw: captures are stored in the lambda closure object, not the coroutine frame. When the lambda is immediately invoked and discarded—a common pattern—the coroutine resumes with dangling references to destroyed captures. This causes undefined behavior that is difficult to diagnose.
This paper proposes a new attribute [[capturewarning]] that library authors can apply to coroutine return types. When a lambda expression returns a type annotated with this attribute, the compiler generates a warning if that lambda has any captures. This provides early detection of a dangerous antipattern without changing language semantics.
1. Introduction
C++20 coroutines introduced a powerful mechanism for writing asynchronous code. However, the interaction between lambda captures and coroutine suspension creates a dangerous pitfall that has bitten many users.
Consider this code:
void process(socket& sock)
{
auto task = [&sock]() -> task<>
{
char buf[1024];
auto [ec, n] = co_await sock.read_some(buffer(buf, sizeof(buf)));
// use data...
}();
run_async(executor)(std::move(task));
}This code has undefined behavior. It may crash, corrupt memory, or appear to work until it doesn't. The problem is subtle and the failure mode is often delayed and non-obvious.
1.1 Why This Fails
When a lambda is invoked:
- The lambda closure is created, capturing
sockby reference - The lambda's
operator()is called - A coroutine frame is allocated on the heap
- The coroutine suspends at
initial_suspend operator()returns thetask<>- The lambda closure is destroyed (it was a temporary)
- Later, the coroutine resumes
- The coroutine tries to access
sockthrough the destroyed closure - Undefined behavior
The critical insight: lambda captures are NOT stored in the coroutine frame. They are stored in the lambda closure object. The coroutine frame contains only a reference to the closure's storage.
1.2 The Scope of the Problem
This issue affects:
- Captures by reference (
[&],[&x]) - Captures by value (
[=],[x])—the copy lives in the lambda closure, not the coroutine frame - Captures of
this—particularly dangerous and common
The problem appears in virtually every async C++ codebase using lambda coroutines. Library authors spend significant effort documenting this pitfall and users repeatedly encounter it.
2. Motivation
2.1 Current Mitigations Are Insufficient
Documentation: Library authors document the issue extensively, but users often discover it only after debugging a crash.
Code review: Human reviewers must memorize and consistently apply this rule. It's easy to miss, especially in large codebases.
Static analysis tools: External tools can detect this pattern, but they are not universally deployed and may have false positives/negatives.
Runtime detection: The undefined behavior often manifests as use-after-free, which tools like AddressSanitizer can detect—but only when the code path is exercised in testing.
2.2 The Safe Pattern Exists But Is Not Enforced
The correct pattern uses function parameters instead of captures:
void process(socket& sock)
{
auto task = [](socket* s) -> task<>
{
char buf[1024];
auto [ec, n] = co_await s->read_some(buffer(buf, sizeof(buf)));
}(&sock); // Pass as argument
run_async(executor)(std::move(task));
}Function parameters ARE copied to the coroutine frame before the first suspension. This Immediately Invoked Lambda Expression (IIFE) pattern is safe but requires discipline to apply consistently.
2.3 Compiler Assistance Is The Right Solution
The compiler already knows:
- Which lambda expressions have captures
- The return type of the lambda's
operator() - Whether that return type has a particular attribute
A simple attribute on coroutine return types would allow library authors to opt in to compile-time warnings, catching this bug class at the earliest possible point.
3. Examples of the Problem
3.1 Basic Capture Dangling
// BROKEN: 'x' captured, lambda destroyed after invoke
void example1()
{
int x = 42;
auto t = [x]() -> task<> {
co_await delay(1s);
std::cout << x; // UB: 'x' was in destroyed lambda
}();
run(std::move(t));
}3.2 Reference Capture Dangling
// BROKEN: Reference to lambda's capture storage, not to 'sock'
void example2(socket& sock)
{
auto t = [&sock]() -> task<> {
co_await sock.connect(endpoint); // UB: dangling reference
}();
run(std::move(t));
}3.3 Capturing this
// BROKEN: 'this' captured in lambda, lambda destroyed after invoke
class connection_handler
{
socket sock_;
std::string name_;
public:
task<> run()
{
return [this]() -> task<>
{
log("Connection from", name_); // UB: 'this' dangles
co_await handle_request();
}();
}
};3.4 Init-Capture Dangling
// BROKEN: Init-capture 'data' lives in lambda closure
void example4()
{
auto t = [data = std::vector<int>{1, 2, 3}]() -> task<> {
co_await delay(1s);
process(data); // UB: data was destroyed
}();
run(std::move(t));
}3.5 Implicit Default Capture
// BROKEN: Implicit capture via [=] or [&]
void example5(int x, socket& s)
{
auto t = [=]() -> task<> {
co_await delay(1s);
use(x); // UB: x was captured in destroyed lambda
}();
auto t2 = [&]() -> task<> {
co_await s.read(); // UB: reference dangles
}();
}3.6 Safe Patterns for Comparison
// SAFE: Parameter copied to coroutine frame
void safe1(socket& sock)
{
auto t = [](socket* s) -> task<> {
co_await s->connect(endpoint); // OK
}(&sock);
run(std::move(t));
}
// SAFE: Lambda outlives coroutine
void safe2(socket& sock)
{
auto handler = [&sock]() -> task<> {
co_await sock.read();
};
run_and_wait(handler()); // Blocks until done
// Lambda destroyed after coroutine completes
}
// SAFE: Member function (this is implicit parameter)
class connection {
socket sock_;
task<> handle() {
co_await sock_.read(); // OK: 'this' is parameter
}
};4. Proposed Solution
We propose a new standard attribute [[capturewarning]] that can be applied to class types. When a lambda expression has a return type that is (or inherits from) a type annotated with [[capturewarning]], and that lambda has any captures, the compiler shall emit a warning diagnostic.
4.1 Design Goals
- Library-controlled: Authors of coroutine libraries opt in by annotating their task types
- Zero runtime cost: Pure compile-time diagnostic
- Non-breaking: A warning, not an error—users can suppress if they know what they're doing
- Simple semantics: Easy to specify and implement
4.2 Example Usage
namespace mylib {
template<class T = void>
struct [[capturewarning]] task {
// coroutine return type implementation
};
} // namespace mylibWith this annotation:
// Warning: lambda returning 'task<>' has captures
auto bad = [x]() -> mylib::task<> {
co_await something();
use(x);
}();
// No warning: no captures
auto good = [](int x) -> mylib::task<> {
co_await something();
use(x);
}(42);
// No warning: not a lambda
mylib::task<> regular_function() {
co_await something();
}5. Proposed Wording
5.1 Attribute Syntax
Add to [dcl.attr.grammar]:
attribute-token:
identifier
attribute-scoped-tokenThe following attribute-tokens are defined:
capturewarning
5.2 Attribute Specification
Add a new subsection [dcl.attr.capturewarning]:
dcl.attr.capturewarning: Capture warning attribute
The attribute-token
capturewarningmay be applied to the definition of a class type. It shall appear at most once in each attribute-list and no attribute-argument-clause shall be present.[Example:
struct [[capturewarning]] task { /* ... */ };—end example]
Semantics
When a lambda-expression whose return type
Tis a class type, and either:
Tis declared with thecapturewarningattribute, orTis derived from a class type declared with thecapturewarningattributeand the lambda-expression has a lambda-capture that is not empty (i.e., captures one or more entities), the implementation should issue a diagnostic.
[Note: This attribute is intended to help detect a common source of undefined behavior where lambda captures are stored in the closure object rather than the coroutine frame, leading to dangling references when the closure is destroyed before coroutine completion. —end note]
[Example:
struct [[capturewarning]] task { /* ... */ }; void f(int x) { // Diagnostic recommended: lambda captures 'x' auto t1 = [x]() -> task { co_return; }(); // Diagnostic recommended: lambda captures 'x' by reference auto t2 = [&x]() -> task { co_return; }(); // No diagnostic: no captures auto t3 = [](int y) -> task { co_return; }(x); // No diagnostic: not a lambda // task regular_coroutine(); }—end example]
Recommended practice
Implementations are encouraged to provide a mechanism to suppress or elevate this diagnostic on a per-occurrence basis.
5.3 Feature Test Macro
Add to [cpp.predefined]:
__cpp_lib_capturewarningwith valueYYYYMML(date of adoption)
6. Implementation Considerations
6.1 Compiler Implementation
The implementation is straightforward:
- When processing a lambda expression, check if it has captures
- If yes, check if the return type (deduced or explicit) has or inherits from a type with
[[capturewarning]] - If yes, emit a warning diagnostic
This requires no new analysis passes—all information is already available during lambda semantic analysis.
6.2 Relationship to Existing Warnings
Some compilers already provide warnings for related patterns:
- Clang's
-Wdanglingfamily - GCC's
-Wdangling-reference - MSVC's lifetime warnings
The [[capturewarning]] attribute complements these by providing library-controlled opt-in for specific types where the pattern is known to be problematic.
6.3 False Positives
The warning may fire in cases where the lambda legitimately outlives the coroutine:
void safe_pattern()
{
int x = 42;
auto handler = [x]() -> task<> { // Warning, but actually safe
co_await delay(1s);
use(x);
};
run_and_wait(handler()); // Blocks until complete
}Users can suppress the warning in these cases using implementation-specific mechanisms or restructure to use parameters.
7. Alternatives Considered
7.1 Language-Level Fix
One could imagine changing the language so that lambda captures ARE stored in the coroutine frame. This would be a significant language change with potential ABI implications and was not pursued in C++20 or C++23. The [[capturewarning]] attribute provides immediate value without requiring such changes.
7.2 Error Instead of Warning
Making this an error would break existing code that correctly manages lambda lifetimes. A warning is the appropriate diagnostic level—it alerts users while allowing them to make informed decisions.
7.3 Standard Library Concept
Instead of an attribute, a concept like capture_unsafe_coroutine<T> could be defined. However, concepts cannot trigger diagnostics on their own, and integrating with lambda analysis would require language changes. An attribute is a cleaner fit.
7.4 Compiler-Specific Attributes
Users could define [[clang::capturewarning]] or [[gnu::capturewarning]] today. Standardization ensures consistent behavior across compilers and establishes a common vocabulary for library authors.
8. Impact on Existing Code
8.1 Backward Compatibility
- Existing code is unaffected unless library authors add the attribute
- Adding the attribute is a pure extension—no source or ABI breakage
- Users who see new warnings can fix their code or suppress the warning
8.2 Library Adoption
Library authors can adopt the attribute immediately upon compiler support:
template<class T>
struct [[capturewarning]] task {
// existing implementation unchanged
};No changes to library semantics or usage patterns are required.
9. Conclusion
The lambda coroutine capture problem is:
- Common: Affects virtually every async C++ codebase
- Dangerous: Causes undefined behavior that is hard to diagnose
- Preventable: The safe pattern (IIFE with parameters) is known
The [[capturewarning]] attribute provides:
- Early detection: Compile-time warning catches bugs before runtime
- Library control: Authors opt in for their coroutine types
- Zero cost: No runtime overhead
- Simple implementation: Straightforward compiler support
This small addition would significantly improve the safety of coroutine-based async programming in C++.
Acknowledgements
Thanks to the authors of coroutine libraries who have documented this pitfall extensively, helping users understand the issue and develop safe patterns.
References
WG21 Papers
-
[P0912R5] Gor Nishanov. Coroutines TS. Incorporated into C++20.
-
[P2300R10] Michał Dominiak, Lewis Baker, Lee Howes, et al. std::execution. https://wg21.link/P2300R10
Technical Resources
-
Lewis Baker. C++ Coroutines: Understanding Symmetric Transfer. https://lewissbaker.github.io/
-
Raymond Chen. C++ coroutines: The problem of the synchronous apartment-changing callback. Microsoft DevBlogs.
Revision History
R0 (2026-01-30)
- Initial revision proposing
[[capturewarning]]attribute for coroutine return types - Documented the lambda coroutine capture problem with examples
- Provided proposed wording for attribute syntax and semantics