Aug 1, 2026

[C++][coroutine] Symmetric Transfer - what problem it solves

The Core Problem: Stack Overflow via Asymmetric Transfer

In early C++ Coroutines (Coroutines TS), resuming a coroutine meant calling .resume() inside `await_suspend()`. When coroutines synchronously complete in a loop or tail-recurse, every .resume()  call pushes a new C++ stack frame without popping the old one, leading to stack overflow.

// ASYMMETRIC TRANSFER (Naive Approach)

void await_suspend(std::coroutine_handle<> h) {
    // Calling .resume() pushes a new stack frame.
    // If done in a deep loop or recursive chain, stack space explodes!
    other_coro_.resume(); 
}


Stack Frame Accumulation:

[ loop_coroutine$resume ]

  └─> [ task::awaiter::await_suspend ]

        └─> [ child_coroutine$resume ]

              └─> [ final_awaiter::await_suspend ]

                    └─> [ loop_coroutine$resume ]  <-- STACK OVERFLOW!



The Solution: Symmetric Transfer

Symmetric transfer allows `await_suspend()` to return a std::coroutine_handle<> instead of void.


Returning a handle suspends the current coroutine frame, pops the current stack frame, and transfers execution directly to the returned handle via a tail call.

Stack usage remains O(1) regardless of how many synchronous suspension/resumes occur.

// SYMMETRIC TRANSFER (Modern C++20)

std::coroutine_handle<> await_suspend(std::coroutine_handle<> h) {
    // Return the handle to transfer control directly.
    // The compiler generates a tail-call: pops current stack frame, then resumes target.
    return other_coro_; 

}


Key Implementations

A. The Awaiter (`task::operator co_await`)

When `co_await child_task;` executes, transfer control directly to the child's handle:

struct task_awaiter {
    std::coroutine_handle<promise_type> child_coro_;
    bool await_ready() noexcept { return false; }

    // Symmetric Transfer: Returns child handle to resume
    std::coroutine_handle<> await_suspend(std::coroutine_handle<> awaiting_coro) noexcept {
        // 1. Store caller as continuation in child's promise
        child_coro_.promise().continuation = awaiting_coro;
        
        // 2. Return child handle -> tail-call into child_coro_
        return child_coro_; 

    }
    void await_resume() noexcept {}
};


B. The Final Suspend (`promise_type::final_suspend`)

When a child coroutine finishes at `co_return`, transfer control back to its continuation (the caller):

struct final_awaiter {
    bool await_ready() noexcept { return false; }

    // Symmetric Transfer: Returns caller's handle to resume
    std::coroutine_handle<> await_suspend(std::coroutine_handle<promise_type> me) noexcept {
        // Returns parent handle -> tail-call back to parent coroutine
        return me.promise().continuation; 
    }
    void await_resume() noexcept {}

};

struct promise_type {
    std::coroutine_handle<> continuation{std::noop_coroutine()};
    final_awaiter final_suspend() noexcept { return {}; }
    // ...

};

Summary Matrix



No comments:

Post a Comment

Note: Only a member of this blog may post a comment.