Skip to content

Instantly share code, notes, and snippets.

@igaztanaga
Last active February 2, 2026 15:17
Show Gist options
  • Select an option

  • Save igaztanaga/906b47b7fe54603821ec1200d5f7176f to your computer and use it in GitHub Desktop.

Select an option

Save igaztanaga/906b47b7fe54603821ec1200d5f7176f to your computer and use it in GitHub Desktop.
C++ Coroutines: Complete Implementation Guide

C++ Coroutines: Complete Implementation Guide

What the Compiler Really Does


Table of Contents

  1. Introduction to C++20 Coroutines
  2. The Coroutine Frame
  3. Coroutine Transformation Overview
  4. The co_await Transformation
  5. Symmetric Transfer
  6. The co_yield Transformation
  7. The co_return Transformation
  8. Complete generator<T> Implementation
  9. Complete task<T> Implementation
  10. Standard Awaiters
  11. Exception Handling in Coroutines
  12. Memory and Lifetime Considerations
  13. Advanced Topics
  14. Debugging and Best Practices
  15. Appendix: Quick Reference

Chapter 1: Introduction to C++20 Coroutines

1.1 What Are Coroutines?

C++20 coroutines are stackless, suspendable functions. Unlike regular functions that must run to completion once called, coroutines can suspend their execution at specific points and be resumed later, potentially by a different thread or execution context.

The key distinction from regular functions:

// Regular function - runs to completion
int regular_function() {
    int x = compute_something();  // Must complete
    int y = compute_more();       // Must complete
    return x + y;                 // Returns, stack frame destroyed
}

// Coroutine - can suspend mid-execution
task<int> coroutine_function() {
    int x = co_await async_compute();  // May suspend here
    int y = co_await async_more();     // May suspend here
    co_return x + y;                   // Completes the coroutine
}

1.2 Stackless vs Stackful Coroutines

C++ coroutines are stackless, meaning they don't preserve the entire call stack when suspended. Instead, they store only the local state needed to resume. This is in contrast to stackful coroutines (like fibers or green threads) that preserve the entire stack.

Advantages of stackless coroutines:

  1. Lower memory overhead - Only necessary state is preserved, not entire stack frames
  2. Predictable allocation - The compiler knows exactly what needs to be stored
  3. Optimization opportunities - The compiler can inline and optimize the state machine

Disadvantages:

  1. Cannot suspend in nested calls - Only the coroutine itself can suspend, not functions it calls
  2. More complex implementation - Requires compiler transformation of the function

1.3 The Three Coroutine Keywords

A function becomes a coroutine when it contains any of these three keywords:

Keyword Purpose Effect
co_await Suspend until awaitable completes May suspend, receives result
co_yield Produce a value and suspend Yields value to caller
co_return Complete the coroutine Signals completion, optional value

The mere presence of any of these keywords transforms the function entirely. The compiler generates a state machine, allocates a coroutine frame, and rewrites all the code.

1.4 The Three Core Types

Every coroutine involves three types working together:

Promise Type

Controls the coroutine's behavior: what happens at the start, at suspension points, at the end, and how values are produced. The promise lives inside the coroutine frame.

Coroutine Handle

A pointer-like type (coroutine_handle<Promise>) that allows external code to resume or destroy the coroutine. It's essentially a type-safe pointer to the coroutine frame.

Awaitable/Awaiter

Objects that define what happens at a co_await expression: whether to suspend, what to do when suspended, and what value to produce when resumed.


Chapter 2: The Coroutine Frame

2.1 Frame Structure

When the compiler encounters a coroutine, it generates a structure called the coroutine frame. This frame is heap-allocated (by default) and contains everything needed to suspend and resume the coroutine.

// Conceptual structure of a coroutine frame
struct __coroutine_frame {
    // Function pointers for resume and destroy operations
    void (*__resume_fn)(__coroutine_frame*);
    void (*__destroy_fn)(__coroutine_frame*);
    
    // The promise object - controls coroutine behavior
    promise_type __promise;
    
    // Current suspension point (which co_await/co_yield we're at)
    int __suspend_index;
    
    // Copies of all function parameters
    Param1 __param1;
    Param2 __param2;
    // ...
    
    // Local variables that need to survive across suspension points
    LocalVar1 __local1;
    LocalVar2 __local2;
    // ...
    
    // Temporary objects for awaiters at each suspension point
    Awaiter1 __awaiter1;
    Awaiter2 __awaiter2;
    // ...
};

2.2 Frame Layout Details

The frame contains several categories of data:

Resume and Destroy Pointers

The first two members are function pointers that implement the resume and destroy operations. When you call handle.resume(), it invokes the resume function pointer with the frame address. This design allows the coroutine handle to be a simple pointer without virtual function overhead.

The Promise Object

The promise object is constructed in-place within the frame. It provides customization points for the coroutine's behavior and often stores the coroutine's result value.

Suspension Index

An integer that tracks which suspension point the coroutine is currently at. This is used by the resume function to jump to the correct location in the state machine.

Parameters

All function parameters are copied (or moved) into the frame. This is necessary because the original parameters on the caller's stack may be gone when the coroutine resumes. For expensive-to-copy types, use references or pointers in the parameter list, but be aware the referred-to objects must outlive the coroutine.

Local Variables

Only local variables that are "live" across a suspension point need to be stored in the frame. The compiler performs liveness analysis to determine which variables need frame storage. Variables used only between two suspension points can remain in registers or on the stack during that execution segment.

2.3 Frame Allocation

By default, the coroutine frame is allocated using operator new. The promise type can customize this:

struct promise_type {
    // Custom allocation - receives coroutine parameters
    void* operator new(std::size_t size) {
        return ::operator new(size);
    }
    
    // Custom allocation with parameters (C++20)
    // Called if available, receives copies of coroutine arguments
    void* operator new(std::size_t size, Args... args) {
        // Can use args to determine allocation strategy
        return custom_allocator.allocate(size);
    }
    
    void operator delete(void* ptr) {
        ::operator delete(ptr);
    }
    
    // Returning a special type can prevent heap allocation
    // The coroutine frame may be placed in the caller's frame
    static auto get_return_object_on_allocation_failure() {
        return task<T>{nullptr};  // Return null task on alloc failure
    }
};

2.4 Heap Allocation Elision (HALO)

The compiler may elide the heap allocation entirely if it can prove:

  1. The lifetime of the coroutine is strictly nested within the caller
  2. The size of the coroutine frame is known at the call site

This optimization is called HALO (Heap Allocation eLision Optimization). When applied, the coroutine frame is placed on the caller's stack, eliminating allocation overhead entirely.

// HALO may apply here - coroutine lifetime is clearly bounded
void process() {
    auto gen = range(1, 10);  // Frame could be on stack
    while (gen.next()) {
        use(gen.value());
    }
}  // gen destroyed here, frame lifetime ends

// HALO unlikely - coroutine escapes
task<int> start_async() {
    auto t = async_operation();  // Frame must be heap-allocated
    schedule_for_later(t);       // Coroutine escapes this scope
    return t;
}

Chapter 3: Coroutine Transformation Overview

3.1 The Ramp Function

When you call a coroutine, you don't directly execute its body. Instead, you execute a compiler-generated ramp function that sets up the coroutine. The ramp function:

  1. Allocates the coroutine frame
  2. Copies parameters into the frame
  3. Constructs the promise object
  4. Obtains the return object (what the caller receives)
  5. Executes initial_suspend()
  6. Returns the return object to the caller

3.2 Complete Transformation Example

Consider this simple coroutine:

task<int> simple_coro(int x) {
    int y = co_await async_read();
    co_return x + y;
}

The compiler transforms this into something conceptually like:

// The coroutine frame structure
struct __simple_coro_frame {
    void (*__resume)(__simple_coro_frame*);
    void (*__destroy)(__simple_coro_frame*);
    task<int>::promise_type __promise;
    int __suspend_index;
    
    // Parameter
    int __x;
    
    // Local that crosses suspension
    int __y;
    
    // Awaiter storage
    decltype(async_read()) __awaitable_1;
    decltype(get_awaiter(__awaitable_1)) __awaiter_1;
};

// The ramp function - what gets called when you invoke simple_coro()
task<int> simple_coro(int x) {
    // Step 1: Allocate the frame
    __simple_coro_frame* __frame = 
        new __simple_coro_frame();
    
    // Step 2: Set up function pointers
    __frame->__resume = &__simple_coro_resume;
    __frame->__destroy = &__simple_coro_destroy;
    
    // Step 3: Copy parameters
    __frame->__x = x;
    
    // Step 4: Construct promise in-place
    new (&__frame->__promise) task<int>::promise_type();
    
    // Step 5: Get return object
    // This is what the caller receives
    task<int> __return_obj = __frame->__promise.get_return_object();
    
    // Step 6: Initial suspend
    auto __initial_awaiter = __frame->__promise.initial_suspend();
    
    if (!__initial_awaiter.await_ready()) {
        __frame->__suspend_index = 0;
        __initial_awaiter.await_suspend(
            std::coroutine_handle<task<int>::promise_type>
                ::from_promise(__frame->__promise)
        );
        // Return to caller - coroutine is suspended
        return __return_obj;
    }
    __initial_awaiter.await_resume();
    
    // If initial_suspend didn't suspend, start executing body
    // (but most coroutines suspend initially)
    __simple_coro_resume(__frame);
    
    return __return_obj;
}

3.3 The Resume Function

The resume function is a state machine that continues execution from the current suspension point:

void __simple_coro_resume(__simple_coro_frame* __frame) {
    try {
        // Jump to current suspension point
        switch (__frame->__suspend_index) {
            case 0: goto __initial_suspend_resume;
            case 1: goto __await_1_resume;
            case 2: goto __final_suspend_resume;
        }
        
    __initial_suspend_resume:
        // After initial_suspend completes
        
        // int y = co_await async_read();
        {
            __frame->__awaitable_1 = async_read();
            __frame->__awaiter_1 = get_awaiter(__frame->__awaitable_1);
            
            if (!__frame->__awaiter_1.await_ready()) {
                __frame->__suspend_index = 1;
                __frame->__awaiter_1.await_suspend(
                    handle_from_promise(__frame->__promise)
                );
                return;  // SUSPEND
            }
        }
        
    __await_1_resume:
        __frame->__y = __frame->__awaiter_1.await_resume();
        
        // co_return x + y;
        __frame->__promise.return_value(__frame->__x + __frame->__y);
        goto __final_suspend;
        
    __final_suspend:
        {
            auto __final_awaiter = __frame->__promise.final_suspend();
            if (!__final_awaiter.await_ready()) {
                __frame->__suspend_index = 2;
                __final_awaiter.await_suspend(
                    handle_from_promise(__frame->__promise)
                );
                return;  // SUSPEND at final
            }
        }
        
    __final_suspend_resume:
        // Destroy the frame
        goto __destroy;
        
    __destroy:
        __frame->__promise.~promise_type();
        delete __frame;
        return;
        
    } catch (...) {
        __frame->__promise.unhandled_exception();
        goto __final_suspend;
    }
}

3.4 The Destroy Function

The destroy function cleans up the coroutine frame without resuming execution:

void __simple_coro_destroy(__simple_coro_frame* __frame) {
    // Destroy local variables based on suspension point
    // Variables are destroyed in reverse order of construction
    switch (__frame->__suspend_index) {
        case 2:  // At final suspend
        case 1:  // After first co_await
            // __y is initialized, destroy it
            // (trivial for int, but matters for objects)
            __frame->__y.~int();
            [[fallthrough]];
        case 0:  // At initial suspend
            // Nothing initialized yet beyond parameters
            break;
    }
    
    // Always destroy promise
    __frame->__promise.~promise_type();
    
    // Deallocate frame
    delete __frame;
}

Chapter 4: The co_await Transformation

4.1 The Awaiter Interface

The co_await expression operates on an awaiter object. An awaiter must provide three member functions:

struct awaiter {
    // Called first: checks if we can skip suspension entirely
    // If returns true, we never suspend - go straight to await_resume
    bool await_ready();
    
    // Called if await_ready() returned false
    // Receives the handle to the current coroutine
    // Return type determines what happens:
    //   void     - always suspend
    //   bool     - suspend only if returns true
    //   handle<> - symmetric transfer to that coroutine
    auto await_suspend(std::coroutine_handle<> h);
    
    // Called when the coroutine resumes (or immediately if we never suspended)
    // The return value becomes the result of the co_await expression
    T await_resume();
};

4.2 Getting the Awaiter

The co_await expr expression doesn't directly use expr as the awaiter. Instead, the compiler follows a specific algorithm to obtain the awaiter:

// Step 1: Get the awaitable
// If promise has await_transform, use it
auto awaitable = promise.await_transform(expr);  // if exists
// Otherwise, use expr directly
auto awaitable = expr;

// Step 2: Get the awaiter from the awaitable
// Try member operator co_await first
auto awaiter = awaitable.operator co_await();  // if exists

// Otherwise try free operator co_await
auto awaiter = operator co_await(awaitable);   // if exists

// Otherwise the awaitable IS the awaiter
auto awaiter = awaitable;

The await_transform hook is powerful - it lets the promise intercept all co_await expressions. This is used to implement features like cancellation tokens or to forbid awaiting certain types.

4.3 Complete co_await Transformation

Here's the complete transformation for a co_await expression:

// Original code:
auto result = co_await some_async_operation();

// Compiler transforms to:
{
    // Step 1: Evaluate the expression and get awaitable
    auto&& __awaitable = some_async_operation();
    
    // Step 2: Apply await_transform if present
    // (pseudo-code for the lookup)
    auto&& __transformed = [&]() {
        if constexpr (has_await_transform<promise_type>) {
            return __promise.await_transform(
                static_cast<decltype(__awaitable)&&>(__awaitable)
            );
        } else {
            return static_cast<decltype(__awaitable)&&>(__awaitable);
        }
    }();
    
    // Step 3: Get the awaiter
    auto&& __awaiter = [&]() {
        if constexpr (has_member_operator_co_await<decltype(__transformed)>) {
            return __transformed.operator co_await();
        } else if constexpr (has_free_operator_co_await<decltype(__transformed)>) {
            return operator co_await(__transformed);
        } else {
            return static_cast<decltype(__transformed)&&>(__transformed);
        }
    }();
    
    // Step 4: Check if we need to suspend
    if (!__awaiter.await_ready()) {
        
        // Step 5: Save suspension point
        __frame->__suspend_index = N;
        
        // Step 6: Call await_suspend and handle its return
        using suspend_result_t = decltype(
            __awaiter.await_suspend(__current_handle)
        );
        
        if constexpr (std::is_void_v<suspend_result_t>) {
            // void return: always suspend
            __awaiter.await_suspend(__current_handle);
            return;  // Return to caller/resumer
        }
        else if constexpr (std::is_same_v<suspend_result_t, bool>) {
            // bool return: suspend only if true
            if (__awaiter.await_suspend(__current_handle)) {
                return;  // Suspend
            }
            // Fall through to await_resume if returned false
        }
        else {
            // coroutine_handle return: symmetric transfer
            auto __next = __awaiter.await_suspend(__current_handle);
            __next.resume();  // Tail call - resume that coroutine
            return;
        }
    }
    
    // Step 7: Resume point - get the result
    __resume_point_N:
    auto result = __awaiter.await_resume();
}

4.4 The await_suspend Return Types

The return type of await_suspend provides flexibility:

void Return

The coroutine always suspends. The awaiter is responsible for eventually resuming the coroutine (or destroying it).

void await_suspend(std::coroutine_handle<> h) {
    // Schedule resumption on a thread pool
    thread_pool.enqueue([h]() { h.resume(); });
    // Coroutine is now suspended
}

bool Return

The coroutine suspends only if true is returned. This allows "just-in-time" cancellation of suspension:

bool await_suspend(std::coroutine_handle<> h) {
    // Try to set up async operation
    // If the operation completed synchronously, return false
    if (try_start_async(h)) {
        return true;   // Suspend - operation pending
    }
    return false;      // Don't suspend - already complete
}

coroutine_handle Return

This enables symmetric transfer - directly resuming another coroutine without growing the call stack. This is crucial for implementing efficient coroutine schedulers.

std::coroutine_handle<> await_suspend(std::coroutine_handle<> h) {
    // Store h for later resumption
    this->continuation = h;
    
    // Return the next coroutine to run
    // The compiler will tail-call resume() on it
    return get_next_ready_coroutine();
}

Chapter 5: Symmetric Transfer

5.1 The Stack Overflow Problem

Consider a chain of coroutines where each awaits the next:

task<int> coro_a() {
    co_return co_await coro_b();  // a awaits b
}

task<int> coro_b() {
    co_return co_await coro_c();  // b awaits c
}

task<int> coro_c() {
    co_return 42;  // c completes
}

When coro_c completes, it must resume coro_b, which then resumes coro_a. With naive implementation, each resume() call adds a stack frame:

// Naive resume chain - each call adds stack frame
// Stack: main
coro_a starts, suspends waiting for coro_b
// Stack: main -> coro_a.resume
coro_b starts, suspends waiting for coro_c
// Stack: main -> coro_a.resume -> coro_b.resume  
coro_c runs, completes, calls coro_b.resume()
// Stack: main -> coro_a.resume -> coro_b.resume -> coro_c.resume -> coro_b.resume
coro_b completes, calls coro_a.resume()
// Stack: ... -> coro_b.resume -> coro_a.resume  <-- GROWING!

With thousands of coroutines in a chain, this causes stack overflow!

5.2 Symmetric Transfer Solution

Symmetric transfer solves this by having await_suspend return a coroutine_handle<>. The compiler transforms this into a tail call, preventing stack growth:

// In the transformed code, when await_suspend returns a handle:
auto __next_handle = __awaiter.await_suspend(__current_handle);

// The compiler generates a TAIL CALL to resume:
// Instead of: __next_handle.resume(); return;
// It generates: TAILCALL __next_handle.resume();

// This reuses the current stack frame instead of adding a new one

5.3 Implementation in task<T>

Here's a complete task type that implements symmetric transfer. Pay attention to how the awaiter struct gets its coro member initialized through operator co_await():

template<typename T>
class task {
public:
    struct promise_type {
        std::coroutine_handle<> continuation;  // Who's waiting for us
        T result;
        
        task get_return_object() {
            return task{
                std::coroutine_handle<promise_type>::from_promise(*this)
            };
        }
        
        std::suspend_always initial_suspend() noexcept { return {}; }
        
        auto final_suspend() noexcept {
            struct final_awaiter {
                bool await_ready() noexcept { return false; }
                
                // Return the continuation handle for symmetric transfer
                std::coroutine_handle<> await_suspend(
                    std::coroutine_handle<promise_type> h
                ) noexcept {
                    if (h.promise().continuation) {
                        // Symmetric transfer to continuation
                        return h.promise().continuation;
                    }
                    // No continuation - return noop handle
                    return std::noop_coroutine();
                }
                
                void await_resume() noexcept {}
            };
            return final_awaiter{};
        }
        
        void return_value(T value) {
            result = std::move(value);
        }
        
        void unhandled_exception() {
            std::terminate();
        }
    };

private:
    std::coroutine_handle<promise_type> handle_;

public:
    // Constructor - stores the coroutine handle
    explicit task(std::coroutine_handle<promise_type> h) : handle_(h) {}
    
    // Move constructor
    task(task&& other) noexcept 
        : handle_(std::exchange(other.handle_, nullptr)) {}
    
    // Destructor - destroys the coroutine frame
    ~task() {
        if (handle_) handle_.destroy();
    }
    
    // Non-copyable
    task(const task&) = delete;
    task& operator=(const task&) = delete;

    // The awaiter returned when you co_await a task
    struct awaiter {
        std::coroutine_handle<promise_type> coro;
        
        bool await_ready() noexcept { return false; }
        
        // Store continuation and return task's handle for symmetric transfer
        std::coroutine_handle<> await_suspend(
            std::coroutine_handle<> h
        ) noexcept {
            coro.promise().continuation = h;
            return coro;  // Resume the task
        }
        
        T await_resume() {
            return std::move(coro.promise().result);
        }
    };
    
    // KEY: This is how the awaiter gets initialized with our handle
    // When you write: co_await some_task
    // The compiler calls: some_task.operator co_await()
    // Which returns: awaiter{handle_}
    awaiter operator co_await() && noexcept {
        return awaiter{handle_};
    }
};

The operator co_await() member function is the bridge between the task and its awaiter. When you write co_await some_task, the compiler calls this operator, which constructs an awaiter initialized with the task's internal coroutine handle. The && qualifier means this only works on rvalue tasks, which is intentional since a task should typically only be awaited once.

5.4 Execution Flow with Symmetric Transfer

With symmetric transfer, here's what happens:

// Stack: main
coro_a() called
  - allocates frame_a, returns task_a
  - initial_suspend suspends
  
caller co_awaits task_a
  - task_a.awaiter.await_suspend stores continuation
  - returns frame_a handle (symmetric transfer)
  - TAIL CALL: frame_a.resume()

// Stack: main (same frame reused!)
coro_a resumes, co_awaits coro_b()
  - coro_b allocates frame_b, returns task_b
  - task_b.awaiter.await_suspend stores frame_a as continuation
  - returns frame_b handle
  - TAIL CALL: frame_b.resume()

// Stack: main (still same frame!)
coro_b resumes, co_awaits coro_c()
  - similar process...
  - TAIL CALL: frame_c.resume()

// Stack: main
coro_c completes, calls return_value
  - final_suspend returns frame_b handle (continuation)
  - TAIL CALL: frame_b.resume()

// Stack: main  
coro_b completes
  - final_suspend returns frame_a handle
  - TAIL CALL: frame_a.resume()

// Stack: main
coro_a completes
  - No more continuations

Key insight: The stack never grows beyond a single resume frame, no matter how deep the coroutine chain!

5.5 The noop_coroutine

std::noop_coroutine() returns a handle to a "do nothing" coroutine. When resumed, it simply returns. This is useful as a sentinel value when there's no continuation:

std::coroutine_handle<> await_suspend(std::coroutine_handle<promise_type> h) {
    if (h.promise().continuation) {
        return h.promise().continuation;  // Resume waiter
    }
    return std::noop_coroutine();  // No one waiting - do nothing
}

Chapter 6: The co_yield Transformation

6.1 co_yield is co_await in Disguise

The co_yield expression is syntactic sugar. The statement:

co_yield value;

Is exactly equivalent to:

co_await promise.yield_value(value);

The promise's yield_value method receives the yielded value, stores it somewhere accessible, and returns an awaiter that typically suspends the coroutine.

6.2 Typical yield_value Implementation

template<typename T>
struct generator<T>::promise_type {
    T* current_value;
    
    auto yield_value(T& value) {
        current_value = std::addressof(value);
        return std::suspend_always{};
    }
    
    auto yield_value(T&& value) {
        current_value = std::addressof(value);
        return std::suspend_always{};
    }
};

6.3 Complete co_yield Transformation

// Original:
co_yield some_value;

// Transforms to:
co_await __promise.yield_value(some_value);

// Which further transforms to the full co_await machinery:
{
    auto&& __awaiter = __promise.yield_value(some_value);
    
    if (!__awaiter.await_ready()) {  // suspend_always returns false
        __frame->__suspend_index = N;
        __awaiter.await_suspend(__current_handle);  // void, always suspends
        return;  // SUSPENDED - control returns to consumer
    }
    
    __resume_point_N:
    __awaiter.await_resume();  // Result is void, discarded
}

// The yielded value is now accessible via promise.current_value
// The coroutine is suspended, waiting to be resumed

6.4 The Consumer's Perspective

From the consumer's perspective:

generator<int> gen = produce_values();

while (gen.move_next()) {  // Calls handle.resume()
    // After resume, the coroutine has:
    // 1. Stored value in promise.current_value
    // 2. Suspended at the co_yield point
    
    int value = gen.current_value();  // Access promise.current_value
    process(value);
    
    // Loop back - next resume() continues from co_yield
}

Chapter 7: The co_return Transformation

7.1 co_return Variants

co_return has two forms:

// Form 1: Return a value
co_return expression;
// Calls: promise.return_value(expression);

// Form 2: Return void
co_return;
// Calls: promise.return_void();

// Also, falling off the end of a coroutine is equivalent to:
co_return;

7.2 Complete co_return Transformation

// Original:
co_return result_value;

// Transforms to:
__promise.return_value(result_value);
goto __final_suspend;

// For void return:
co_return;

// Transforms to:
__promise.return_void();
goto __final_suspend;

// The final_suspend block:
__final_suspend:
{
    auto __final_awaiter = __promise.final_suspend();
    
    if (!__final_awaiter.await_ready()) {
        __frame->__suspend_index = FINAL_SUSPEND_INDEX;
        
        // Handle await_suspend return type
        auto __result = __final_awaiter.await_suspend(__current_handle);
        
        if constexpr (returns_handle) {
            __result.resume();  // Symmetric transfer
        }
        return;  // Suspended at final suspend point
    }
    
    // If final_suspend didn't suspend, destroy immediately
    goto __destroy_frame;
}

__destroy_frame:
    __promise.~promise_type();
    delete __frame;

7.3 Why final_suspend Exists

final_suspend() is called after the coroutine body completes but before the frame is destroyed. This is crucial for:

  1. Accessing the result: If final_suspend() returns suspend_always, the coroutine frame remains valid, allowing the caller to access the result stored in the promise.

  2. Symmetric transfer: final_suspend can return a coroutine_handle to resume the continuation, enabling efficient task chaining.

  3. Resource cleanup: The awaiter can perform cleanup before the frame is destroyed.

7.4 Common final_suspend Patterns

// Pattern 1: Simple generator - always suspend
// Allows caller to detect completion via done()
auto final_suspend() noexcept {
    return std::suspend_always{};
}

// Pattern 2: Eager task - never suspend
// Frame destroyed immediately after completion
auto final_suspend() noexcept {
    return std::suspend_never{};  // DANGER: Can't access result!
}

// Pattern 3: Lazy task with symmetric transfer
auto final_suspend() noexcept {
    struct final_awaiter {
        bool await_ready() noexcept { return false; }
        
        std::coroutine_handle<> await_suspend(
            std::coroutine_handle<promise_type> h
        ) noexcept {
            return h.promise().continuation;
        }
        
        void await_resume() noexcept {}
    };
    return final_awaiter{};
}

Chapter 8: Complete generator<T> Implementation

8.1 Overview

A generator<T> is a coroutine type for lazy, pull-based sequences. It's one of the simplest and most useful coroutine types. Let's build a complete, production-quality implementation.

8.2 The Promise Type

#include <coroutine>
#include <exception>
#include <utility>
#include <type_traits>

template<typename T>
class generator {
public:
    struct promise_type {
        // Storage for the current value
        // Using a union to avoid default construction
        union {
            T value_;
        };
        std::exception_ptr exception_;
        
        // Constructor - don't initialize union member
        promise_type() noexcept {}
        
        // Destructor - value is destroyed when we move past co_yield
        ~promise_type() {
            // Note: value_ destroyed elsewhere if needed
        }
        
        // Return the generator object to the caller
        generator get_return_object() {
            return generator{
                std::coroutine_handle<promise_type>::from_promise(*this)
            };
        }
        
        // Always suspend at start - lazy evaluation
        std::suspend_always initial_suspend() noexcept {
            return {};
        }
        
        // Always suspend at end - so we can detect completion
        std::suspend_always final_suspend() noexcept {
            return {};
        }
        
        // Handle co_yield
        template<typename U>
            requires std::convertible_to<U&&, T>
        std::suspend_always yield_value(U&& value) 
            noexcept(std::is_nothrow_constructible_v<T, U&&>) 
        {
            // Construct value in-place
            std::construct_at(std::addressof(value_), std::forward<U>(value));
            return {};
        }
        
        // Generators don't return values
        void return_void() noexcept {}
        
        // Store exception for later rethrowing
        void unhandled_exception() {
            exception_ = std::current_exception();
        }
        
        // Rethrow any stored exception
        void rethrow_if_exception() {
            if (exception_) {
                std::rethrow_exception(exception_);
            }
        }
    };

8.3 The Generator Class

private:
    std::coroutine_handle<promise_type> handle_;
    
public:
    // Constructors
    generator() noexcept : handle_(nullptr) {}
    
    explicit generator(std::coroutine_handle<promise_type> h) noexcept 
        : handle_(h) {}
    
    // Move-only type
    generator(generator&& other) noexcept 
        : handle_(std::exchange(other.handle_, nullptr)) {}
    
    generator& operator=(generator&& other) noexcept {
        if (this != &other) {
            if (handle_) {
                handle_.destroy();
            }
            handle_ = std::exchange(other.handle_, nullptr);
        }
        return *this;
    }
    
    // Non-copyable
    generator(const generator&) = delete;
    generator& operator=(const generator&) = delete;
    
    // Destructor
    ~generator() {
        if (handle_) {
            handle_.destroy();
        }
    }

8.4 Iterator Interface

    // Iterator for range-based for loops
    class iterator {
    public:
        using iterator_category = std::input_iterator_tag;
        using difference_type = std::ptrdiff_t;
        using value_type = T;
        using reference = T&;
        using pointer = T*;
        
    private:
        std::coroutine_handle<promise_type> handle_;
        
    public:
        iterator() noexcept : handle_(nullptr) {}
        
        explicit iterator(std::coroutine_handle<promise_type> h) noexcept 
            : handle_(h) {}
        
        // Pre-increment: advance to next value
        iterator& operator++() {
            // Destroy previous value
            std::destroy_at(std::addressof(handle_.promise().value_));
            
            // Resume coroutine to get next value
            handle_.resume();
            
            // Check for exception
            handle_.promise().rethrow_if_exception();
            
            return *this;
        }
        
        // Post-increment (input iterator requirement)
        void operator++(int) {
            ++(*this);
        }
        
        // Dereference: get current value
        reference operator*() const {
            return handle_.promise().value_;
        }
        
        pointer operator->() const {
            return std::addressof(handle_.promise().value_);
        }
        
        // Equality: check if at end
        bool operator==(std::default_sentinel_t) const noexcept {
            return !handle_ || handle_.done();
        }
        
        bool operator!=(std::default_sentinel_t s) const noexcept {
            return !(*this == s);
        }
    };
    
    // Begin: start iteration, get first value
    iterator begin() {
        if (handle_) {
            handle_.resume();  // Execute until first co_yield
            handle_.promise().rethrow_if_exception();
        }
        return iterator{handle_};
    }
    
    // End: sentinel value
    std::default_sentinel_t end() noexcept {
        return {};
    }

8.5 Manual Iteration Interface

    // Check if generator has more values
    bool next() {
        if (handle_ && !handle_.done()) {
            handle_.resume();
            handle_.promise().rethrow_if_exception();
            return !handle_.done();
        }
        return false;
    }
    
    // Get current value (undefined if done() is true)
    T& value() {
        return handle_.promise().value_;
    }
    
    const T& value() const {
        return handle_.promise().value_;
    }
    
    // Check if coroutine is finished
    bool done() const noexcept {
        return !handle_ || handle_.done();
    }
    
    // Access the underlying handle (for advanced usage)
    std::coroutine_handle<promise_type> handle() const noexcept {
        return handle_;
    }
    
    // Release ownership of the handle
    std::coroutine_handle<promise_type> release() noexcept {
        return std::exchange(handle_, nullptr);
    }
};  // End of generator class

8.6 Usage Examples

// Simple integer range
generator<int> range(int start, int end) {
    for (int i = start; i < end; ++i) {
        co_yield i;
    }
}

// Fibonacci sequence
generator<long long> fibonacci(int count) {
    long long a = 0, b = 1;
    for (int i = 0; i < count; ++i) {
        co_yield a;
        auto next = a + b;
        a = b;
        b = next;
    }
}

// Transform another generator
template<typename T, typename F>
generator<std::invoke_result_t<F, T&>> transform(generator<T> gen, F func) {
    for (auto& value : gen) {
        co_yield func(value);
    }
}

// Filter elements
template<typename T, typename Pred>
generator<T> filter(generator<T> gen, Pred pred) {
    for (auto& value : gen) {
        if (pred(value)) {
            co_yield value;
        }
    }
}

// Usage
int main() {
    // Range-based for loop
    for (int i : range(1, 10)) {
        std::cout << i << " ";
    }
    // Output: 1 2 3 4 5 6 7 8 9
    
    // Manual iteration
    auto fib = fibonacci(10);
    while (fib.next()) {
        std::cout << fib.value() << " ";
    }
    // Output: 0 1 1 2 3 5 8 13 21 34
    
    // Composition
    auto squares = transform(range(1, 5), [](int x) { return x * x; });
    for (int sq : squares) {
        std::cout << sq << " ";
    }
    // Output: 1 4 9 16
}

Chapter 9: Complete task<T> Implementation

9.1 Overview

A task<T> represents an asynchronous operation that produces a value of type T. Unlike generator, task is eager about execution but lazy about starting - it begins execution when awaited.

9.2 The Promise Type

#include <coroutine>
#include <exception>
#include <variant>
#include <utility>

template<typename T>
class task {
public:
    struct promise_type {
        // Store either the result or an exception
        std::variant<std::monostate, T, std::exception_ptr> result_;
        std::coroutine_handle<> continuation_;  // Who's waiting for us
        
        task get_return_object() {
            return task{
                std::coroutine_handle<promise_type>::from_promise(*this)
            };
        }
        
        // Suspend immediately - lazy start
        std::suspend_always initial_suspend() noexcept {
            return {};
        }
        
        // Custom final_suspend for symmetric transfer
        auto final_suspend() noexcept {
            struct final_awaiter {
                bool await_ready() noexcept { return false; }
                
                std::coroutine_handle<> await_suspend(
                    std::coroutine_handle<promise_type> h
                ) noexcept {
                    // Resume whoever is waiting for us
                    if (h.promise().continuation_) {
                        return h.promise().continuation_;
                    }
                    return std::noop_coroutine();
                }
                
                void await_resume() noexcept {}
            };
            return final_awaiter{};
        }
        
        // Store the return value
        template<typename U>
            requires std::convertible_to<U&&, T>
        void return_value(U&& value) {
            result_.template emplace<1>(std::forward<U>(value));
        }
        
        void unhandled_exception() {
            result_.template emplace<2>(std::current_exception());
        }
        
        // Get the result, rethrowing any exception
        T& get_result() & {
            if (result_.index() == 2) {
                std::rethrow_exception(std::get<2>(result_));
            }
            return std::get<1>(result_);
        }
        
        T&& get_result() && {
            if (result_.index() == 2) {
                std::rethrow_exception(std::get<2>(result_));
            }
            return std::move(std::get<1>(result_));
        }
    };

9.3 The Task Class

private:
    std::coroutine_handle<promise_type> handle_;
    
public:
    task() noexcept : handle_(nullptr) {}
    
    explicit task(std::coroutine_handle<promise_type> h) noexcept 
        : handle_(h) {}
    
    // Move-only
    task(task&& other) noexcept 
        : handle_(std::exchange(other.handle_, nullptr)) {}
    
    task& operator=(task&& other) noexcept {
        if (this != &other) {
            if (handle_) handle_.destroy();
            handle_ = std::exchange(other.handle_, nullptr);
        }
        return *this;
    }
    
    task(const task&) = delete;
    task& operator=(const task&) = delete;
    
    ~task() {
        if (handle_) handle_.destroy();
    }
    
    // Check if valid
    explicit operator bool() const noexcept {
        return handle_ != nullptr;
    }
    
    bool done() const noexcept {
        return !handle_ || handle_.done();
    }

9.4 The Awaiter

    // Awaiter returned when you co_await a task
    struct awaiter {
        std::coroutine_handle<promise_type> handle_;
        
        bool await_ready() noexcept {
            return false;  // Always suspend to start the task
        }
        
        // Symmetric transfer: resume the task, store continuation
        std::coroutine_handle<> await_suspend(
            std::coroutine_handle<> continuation
        ) noexcept {
            handle_.promise().continuation_ = continuation;
            return handle_;  // Start/resume the task
        }
        
        // Get the result when we resume
        T await_resume() {
            return std::move(handle_.promise()).get_result();
        }
    };
    
    // Support co_await on task
    awaiter operator co_await() && noexcept {
        return awaiter{handle_};
    }
    
    // For non-coroutine contexts: block until complete
    T sync_wait() {
        // Simple spin wait - in production, use proper synchronization
        struct sync_awaiter {
            bool await_ready() noexcept { return false; }
            void await_suspend(std::coroutine_handle<> h) noexcept {
                // Just resume immediately in same thread
                h.resume();
            }
            void await_resume() noexcept {}
        };
        
        // Resume until done
        while (!handle_.done()) {
            handle_.resume();
        }
        
        return std::move(handle_.promise()).get_result();
    }
};

9.5 Specialization for void

// Specialization for task<void>
template<>
class task<void> {
public:
    struct promise_type {
        std::exception_ptr exception_;
        std::coroutine_handle<> continuation_;
        
        task get_return_object() {
            return task{
                std::coroutine_handle<promise_type>::from_promise(*this)
            };
        }
        
        std::suspend_always initial_suspend() noexcept { return {}; }
        
        auto final_suspend() noexcept {
            struct final_awaiter {
                bool await_ready() noexcept { return false; }
                std::coroutine_handle<> await_suspend(
                    std::coroutine_handle<promise_type> h
                ) noexcept {
                    if (h.promise().continuation_)
                        return h.promise().continuation_;
                    return std::noop_coroutine();
                }
                void await_resume() noexcept {}
            };
            return final_awaiter{};
        }
        
        void return_void() noexcept {}
        
        void unhandled_exception() {
            exception_ = std::current_exception();
        }
        
        void get_result() {
            if (exception_) std::rethrow_exception(exception_);
        }
    };
    
    // ... rest similar to task<T>
};

9.6 Usage Examples

// Simulated async operation
task<int> async_compute(int x) {
    // In real code, this would suspend and resume when I/O completes
    co_return x * 2;
}

// Composition
task<int> compute_sum(int a, int b) {
    int x = co_await async_compute(a);
    int y = co_await async_compute(b);
    co_return x + y;
}

// Sequential operations
task<void> do_work() {
    auto result = co_await compute_sum(10, 20);
    std::cout << "Result: " << result << std::endl;
    co_return;
}

// Error handling
task<int> may_fail(bool should_fail) {
    if (should_fail) {
        throw std::runtime_error("Operation failed");
    }
    co_return 42;
}

task<void> handle_errors() {
    try {
        auto result = co_await may_fail(true);
        std::cout << "Got: " << result << std::endl;
    } catch (const std::exception& e) {
        std::cout << "Error: " << e.what() << std::endl;
    }
}

int main() {
    auto t = compute_sum(10, 20);
    int result = t.sync_wait();  // 60
}

Chapter 10: Standard Awaiters

10.1 suspend_always

The simplest awaiter - always suspends:

struct suspend_always {
    // Never ready - always suspend
    constexpr bool await_ready() const noexcept { 
        return false; 
    }
    
    // Do nothing special on suspend
    constexpr void await_suspend(std::coroutine_handle<>) const noexcept {}
    
    // No result
    constexpr void await_resume() const noexcept {}
};

Used for:

  • initial_suspend() when you want lazy coroutines
  • final_suspend() when you want to detect completion
  • yield_value() for generators

10.2 suspend_never

The opposite - never suspends:

struct suspend_never {
    // Always ready - never suspend
    constexpr bool await_ready() const noexcept { 
        return true; 
    }
    
    // Never called since await_ready returns true
    constexpr void await_suspend(std::coroutine_handle<>) const noexcept {}
    
    // No result
    constexpr void await_resume() const noexcept {}
};

Used for:

  • initial_suspend() when you want eager coroutines
  • Rarely for final_suspend() (frame destroyed immediately)

10.3 coroutine_handle<>

The type-erased handle to any coroutine:

template<typename Promise = void>
struct coroutine_handle;

// Specialization for type-erased handle
template<>
struct coroutine_handle<void> {
    // Construction
    constexpr coroutine_handle() noexcept = default;
    constexpr coroutine_handle(std::nullptr_t) noexcept;
    
    // Assignment
    coroutine_handle& operator=(std::nullptr_t) noexcept;
    
    // Conversion from typed handle
    template<typename Promise>
    coroutine_handle(coroutine_handle<Promise> h) noexcept;
    
    // Get raw address (for storage)
    constexpr void* address() const noexcept;
    
    // Recreate from raw address
    static constexpr coroutine_handle from_address(void* addr) noexcept;
    
    // Check if valid
    constexpr explicit operator bool() const noexcept;
    
    // Resume execution
    void resume() const;
    void operator()() const;  // Same as resume()
    
    // Check if at final suspend point
    bool done() const noexcept;
    
    // Destroy the coroutine
    void destroy() const;
};

// Typed handle - provides access to promise
template<typename Promise>
struct coroutine_handle {
    // Everything from coroutine_handle<void>, plus:
    
    // Get the promise object
    Promise& promise() const noexcept;
    
    // Create from promise
    static coroutine_handle from_promise(Promise& p) noexcept;
};

10.4 noop_coroutine

A coroutine that does nothing when resumed:

// Returns a handle to a no-op coroutine
std::noop_coroutine_handle noop_coroutine() noexcept;

// The handle type
struct noop_coroutine_handle : coroutine_handle<noop_coroutine_promise> {
    // Always returns true (never needs to run)
    constexpr bool done() const noexcept { return true; }
    
    // Resume is a no-op
    constexpr void resume() const noexcept {}
    constexpr void operator()() const noexcept {}
    
    // Cannot be destroyed
    constexpr void destroy() const noexcept {}
};

Used as a sentinel when there's no continuation to resume in symmetric transfer.


Chapter 11: Exception Handling in Coroutines

11.1 How Exceptions Propagate

When an exception escapes the coroutine body, the compiler catches it and calls unhandled_exception():

// Compiler-generated try-catch wrapper
void __coroutine_resume(__frame* f) {
    try {
        // ... coroutine body ...
    } catch (...) {
        f->__promise.unhandled_exception();
    }
    // Proceed to final_suspend regardless
    auto awaiter = f->__promise.final_suspend();
    // ...
}

11.2 Storing Exceptions

struct promise_type {
    std::exception_ptr exception_;
    
    void unhandled_exception() {
        // Capture the current exception
        exception_ = std::current_exception();
    }
    
    // Later, when result is requested:
    T get_result() {
        if (exception_) {
            std::rethrow_exception(exception_);
        }
        return result_;
    }
};

11.3 Exception Timing

Important: The exception is NOT rethrown immediately. It's stored and rethrown when:

  • The result is accessed (for task<T>)
  • The iterator is incremented (for generator<T>)
  • sync_wait() is called
task<int> may_throw() {
    throw std::runtime_error("oops");
    co_return 42;  // Never reached
}

task<void> caller() {
    auto t = may_throw();  // No throw yet!
    // Exception is thrown HERE when we await:
    int x = co_await t;    // Throws!
}

11.4 noexcept Considerations

Critical: final_suspend() should be noexcept. If it throws, std::terminate() is called.

struct promise_type {
    // MUST be noexcept - throwing here calls terminate()
    auto final_suspend() noexcept {
        return std::suspend_always{};
    }
    
    // await_suspend in final_awaiter should also be noexcept
    struct final_awaiter {
        bool await_ready() noexcept { return false; }
        
        std::coroutine_handle<> await_suspend(
            std::coroutine_handle<promise_type> h
        ) noexcept {  // <- noexcept!
            return h.promise().continuation_;
        }
        
        void await_resume() noexcept {}
    };
};

Chapter 12: Memory and Lifetime Considerations

12.1 Coroutine Frame Lifetime

The coroutine frame exists from allocation until:

  • destroy() is called on the handle
  • The coroutine runs to completion without suspending at final_suspend
generator<int> gen = some_generator();
// Frame exists here

gen.begin();  // Resume coroutine
gen.end();    // Check completion

// Frame still exists! (suspended at final_suspend or done)

// Frame destroyed here when gen goes out of scope
// (generator destructor calls handle.destroy())

12.2 Dangling Reference Pitfalls

Parameters are copied into the frame. References to caller's locals are dangerous:

// DANGEROUS: Reference to local
task<void> dangerous(const std::string& s) {
    co_await something();
    // s might be dangling here!
    std::cout << s << std::endl;
}

void caller() {
    std::string local = "hello";
    auto t = dangerous(local);
    // t now holds reference to local
}  // local destroyed!
// t still holds dangling reference

// SAFE: Take by value
task<void> safe(std::string s) {
    co_await something();
    std::cout << s << std::endl;  // s is copied into frame
}

12.3 Lambda Captures

Lambda coroutines with captures are especially dangerous:

// VERY DANGEROUS
auto make_coro(int x) {
    return [x]() -> task<int> {
        co_await something();
        co_return x;  // x is captured in lambda, NOT frame
    }();  // Lambda temporary destroyed after this!
}

// The lambda object (with its captures) is destroyed
// after the coroutine is created. The capture 'x' is gone!

// SAFE: Use explicit parameters
auto make_coro_safe(int x) {
    return [](int x) -> task<int> {
        co_await something();
        co_return x;  // x is a parameter, copied to frame
    }(x);
}

12.4 Symmetric Transfer and Lifetimes

With symmetric transfer, frame lifetimes can be surprising:

task<int> inner() {
    co_return 42;
}

task<int> outer() {
    // After this co_await, inner's frame may be destroyed!
    // (depends on final_suspend behavior)
    int x = co_await inner();
    co_return x;
}

// Timeline:
// 1. outer() creates outer_frame
// 2. outer suspends, inner() creates inner_frame
// 3. inner completes, stores result in promise
// 4. inner's final_suspend does symmetric transfer to outer
// 5. outer resumes, gets result from inner's promise
// 6. inner_frame may be destroyed (if final_suspend so designed)
// 7. outer continues with x

Chapter 13: Advanced Topics

13.1 Custom Allocators

template<typename Allocator>
struct promise_with_allocator {
    Allocator alloc_;
    
    // Receive allocator as first argument
    template<typename... Args>
    promise_with_allocator(Allocator alloc, Args&&...)
        : alloc_(alloc) {}
    
    // Custom allocation
    void* operator new(std::size_t size, Allocator alloc, auto&&...) {
        using traits = std::allocator_traits<Allocator>;
        using rebound = typename traits::template rebind_alloc<std::byte>;
        rebound byte_alloc(alloc);
        
        // Allocate extra space to store allocator
        auto ptr = traits::allocate(byte_alloc, size + sizeof(rebound));
        new (ptr) rebound(byte_alloc);  // Store allocator
        return ptr + sizeof(rebound);
    }
    
    void operator delete(void* ptr, std::size_t size) {
        using traits = std::allocator_traits<Allocator>;
        using rebound = typename traits::template rebind_alloc<std::byte>;
        
        // Recover allocator
        auto alloc_ptr = static_cast<std::byte*>(ptr) - sizeof(rebound);
        rebound alloc = *reinterpret_cast<rebound*>(alloc_ptr);
        
        traits::deallocate(alloc, alloc_ptr, size + sizeof(rebound));
    }
};

13.2 await_transform for Cancellation

struct cancellable_promise {
    bool cancelled_ = false;
    
    // Intercept all co_await expressions
    template<typename T>
    auto await_transform(T&& awaitable) {
        struct cancellation_awaiter {
            T awaitable_;
            bool& cancelled_;
            
            bool await_ready() {
                return cancelled_ || get_awaiter(awaitable_).await_ready();
            }
            
            auto await_suspend(std::coroutine_handle<> h) {
                if (cancelled_) {
                    return false;  // Don't suspend, we're cancelled
                }
                return get_awaiter(awaitable_).await_suspend(h);
            }
            
            auto await_resume() {
                if (cancelled_) {
                    throw cancelled_exception{};
                }
                return get_awaiter(awaitable_).await_resume();
            }
        };
        
        return cancellation_awaiter{
            std::forward<T>(awaitable), 
            cancelled_
        };
    }
};

13.3 Recursive Generators (Nested yield_value)

template<typename T>
class recursive_generator {
    struct promise_type {
        recursive_generator* root_ = this;
        promise_type* parent_ = nullptr;
        promise_type* leaf_ = this;
        T* value_;
        
        // Yield a single value
        std::suspend_always yield_value(T& value) {
            root_->leaf_->value_ = std::addressof(value);
            return {};
        }
        
        // Yield from another generator
        auto yield_value(recursive_generator<T> gen) {
            struct nested_awaiter {
                recursive_generator<T> gen_;
                promise_type* parent_;
                
                bool await_ready() { return false; }
                
                std::coroutine_handle<> await_suspend(
                    std::coroutine_handle<promise_type> h
                ) {
                    auto& nested = gen_.handle_.promise();
                    nested.root_ = h.promise().root_;
                    nested.parent_ = &h.promise();
                    h.promise().root_->leaf_ = &nested;
                    return gen_.handle_;
                }
                
                void await_resume() {}
            };
            return nested_awaiter{std::move(gen), this};
        }
    };
    // ... rest of implementation
};

Chapter 14: Debugging and Best Practices

14.1 Common Mistakes

1. Forgetting that parameters are copied:

// Bug: temporary string destroyed before coroutine runs
task<void> greet(std::string_view name) {  // string_view to temporary!
    co_await delay(1s);
    std::cout << name;  // Dangling!
}
greet(std::string("Bob"));  // Temporary destroyed immediately

2. Destroying a running coroutine:

{
    auto gen = produce();
    gen.begin();  // Start iteration
}  // Destroyed while suspended - OK for generators, but be careful!

3. Double resumption:

auto h = some_coroutine();
h.resume();  // OK
h.resume();  // Might be UB if it completed!

// Always check done() first:
if (!h.done()) h.resume();

14.2 Debugging Tips

1. Add logging to promise methods:

struct promise_type {
    promise_type() {
        std::cout << "Promise constructed at " << this << std::endl;
    }
    
    ~promise_type() {
        std::cout << "Promise destroyed at " << this << std::endl;
    }
    
    auto initial_suspend() {
        std::cout << "initial_suspend" << std::endl;
        return std::suspend_always{};
    }
    // ... etc
};

2. Use address sanitizer to catch use-after-free

3. Validate handle before use:

void safe_resume(std::coroutine_handle<> h) {
    if (h && !h.done()) {
        h.resume();
    }
}

14.3 Best Practices

1. Always suspend at final_suspend

This allows detecting completion and accessing results safely.

2. Make final_suspend noexcept

Throwing from final_suspend calls std::terminate.

3. Use symmetric transfer for task chains

Prevents stack overflow in deep coroutine chains.

4. Take parameters by value for coroutines

Unless you can guarantee the referred object outlives the coroutine.

5. Be explicit about ownership

Document whether the caller or callee owns the coroutine handle.

6. Consider using RAII wrappers

Like generator<T> and task<T> that manage the handle lifetime.

14.4 Performance Considerations

Frame allocation: Each coroutine allocates a frame. For hot paths, consider:

  • HALO optimization eligibility
  • Custom allocators with memory pools
  • Reusing coroutines instead of creating new ones

Suspension overhead: Each suspend/resume has some cost:

  • Prefer batching work between suspensions
  • Use await_ready to short-circuit when possible
  • Symmetric transfer eliminates stack manipulation overhead

Appendix A: Quick Reference

Promise Requirements

Method Required Description
get_return_object() Yes Creates return object for caller
initial_suspend() Yes Awaiter for start of coroutine
final_suspend() Yes Awaiter for end (must be noexcept)
unhandled_exception() Yes Called when exception escapes
return_value(T) If co_return val Called for co_return with value
return_void() If co_return; Called for co_return without value
yield_value(T) If co_yield Called for co_yield, returns awaiter
await_transform(T) No Transform co_await expressions

Awaiter Requirements

Method Description
await_ready() Returns bool: true to skip suspension
await_suspend(handle) Called on suspend. Returns void/bool/handle
await_resume() Called on resume. Returns co_await result

Standard Library Types

Type Description
std::coroutine_handle<P> Handle to coroutine with promise P
std::coroutine_handle<> Type-erased coroutine handle
std::suspend_always Awaiter that always suspends
std::suspend_never Awaiter that never suspends
std::noop_coroutine() Returns handle to no-op coroutine

Awaitable to Awaiter Lookup

co_await expr
    │
    ▼
┌─────────────────────────────────────┐
│ promise.await_transform(expr)?      │
│   Yes → use transformed             │
│   No  → use expr                    │
└─────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────┐
│ awaitable.operator co_await()?      │
│   Yes → use result                  │
│   No  → check free function         │
└─────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────┐
│ operator co_await(awaitable)?       │
│   Yes → use result                  │
│   No  → awaitable IS the awaiter    │
└─────────────────────────────────────┘

Coroutine Lifecycle

┌─────────────────────────────────────────────────────────────────┐
│                    COROUTINE LIFECYCLE                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. CALL: Allocate frame, copy params, construct promise        │
│           ↓                                                     │
│  2. get_return_object() → return object for caller              │
│           ↓                                                     │
│  3. initial_suspend() → usually suspend (lazy) or continue      │
│           ↓                                                     │
│  4. BODY: Execute until co_await/co_yield/co_return             │
│           ↓                                                     │
│     co_await: await_ready? → no → await_suspend → SUSPEND       │
│                           → yes → await_resume → continue       │
│           ↓                                                     │
│     co_yield: yield_value() → co_await machinery → SUSPEND      │
│           ↓                                                     │
│     co_return: return_value/return_void → goto final            │
│           ↓                                                     │
│  5. final_suspend() → usually suspend (don't auto-destroy)      │
│           ↓                                                     │
│  6. DESTROY: Called explicitly via handle.destroy()             │
│              Destroys locals, promise, deallocates frame        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

End of Document

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment