- Introduction to C++20 Coroutines
- The Coroutine Frame
- Coroutine Transformation Overview
- The co_await Transformation
- Symmetric Transfer
- The co_yield Transformation
- The co_return Transformation
- Complete generator<T> Implementation
- Complete task<T> Implementation
- Standard Awaiters
- Exception Handling in Coroutines
- Memory and Lifetime Considerations
- Advanced Topics
- Debugging and Best Practices
- Appendix: Quick Reference
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
}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:
- Lower memory overhead - Only necessary state is preserved, not entire stack frames
- Predictable allocation - The compiler knows exactly what needs to be stored
- Optimization opportunities - The compiler can inline and optimize the state machine
Disadvantages:
- Cannot suspend in nested calls - Only the coroutine itself can suspend, not functions it calls
- More complex implementation - Requires compiler transformation of the function
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.
Every coroutine involves three types working together:
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.
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.
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.
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;
// ...
};The frame contains several categories of data:
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 is constructed in-place within the frame. It provides customization points for the coroutine's behavior and often stores the coroutine's result value.
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.
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.
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.
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
}
};The compiler may elide the heap allocation entirely if it can prove:
- The lifetime of the coroutine is strictly nested within the caller
- 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;
}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:
- Allocates the coroutine frame
- Copies parameters into the frame
- Constructs the promise object
- Obtains the return object (what the caller receives)
- Executes
initial_suspend() - Returns the return object to the caller
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;
}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;
}
}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;
}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();
};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.
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();
}The return type of await_suspend provides flexibility:
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
}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
}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();
}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!
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 oneHere'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.
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 continuationsKey insight: The stack never grows beyond a single resume frame, no matter how deep the coroutine chain!
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
}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.
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{};
}
};// 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 resumedFrom 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
}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;// 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;final_suspend() is called after the coroutine body completes but before the frame is destroyed. This is crucial for:
-
Accessing the result: If
final_suspend()returnssuspend_always, the coroutine frame remains valid, allowing the caller to access the result stored in the promise. -
Symmetric transfer:
final_suspendcan return acoroutine_handleto resume the continuation, enabling efficient task chaining. -
Resource cleanup: The awaiter can perform cleanup before the frame is destroyed.
// 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{};
}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.
#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_);
}
}
};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();
}
} // 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 {};
} // 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// 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
}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.
#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_));
}
};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();
} // 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();
}
};// 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>
};// 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
}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 coroutinesfinal_suspend()when you want to detect completionyield_value()for generators
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)
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;
};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.
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();
// ...
}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_;
}
};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!
}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 {}
};
};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())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
}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);
}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 xtemplate<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));
}
};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_
};
}
};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
};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 immediately2. 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();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();
}
}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.
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_readyto short-circuit when possible - Symmetric transfer eliminates stack manipulation overhead
| 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 |
| 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 |
| 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 |
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 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 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