Skip to content

Implement [[capturewarning]] #4

@vinniefalco

Description

@vinniefalco

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:

  1. The lambda closure is created, capturing sock by reference
  2. The lambda's operator() is called
  3. A coroutine frame is allocated on the heap
  4. The coroutine suspends at initial_suspend
  5. operator() returns the task<>
  6. The lambda closure is destroyed (it was a temporary)
  7. Later, the coroutine resumes
  8. The coroutine tries to access sock through the destroyed closure
  9. 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 mylib

With 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-token

The 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 capturewarning may 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 T is a class type, and either:

  • T is declared with the capturewarning attribute, or
  • T is derived from a class type declared with the capturewarning attribute

and 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_capturewarning with value YYYYMML (date of adoption)


6. Implementation Considerations

6.1 Compiler Implementation

The implementation is straightforward:

  1. When processing a lambda expression, check if it has captures
  2. If yes, check if the return type (deduced or explicit) has or inherits from a type with [[capturewarning]]
  3. 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 -Wdangling family
  • 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions