C++ Coroutines Cheat Sheet
co_await/co_yield/co_return keywords, promise_type machinery, and how to build a minimal generator or task type in C++20.
The Three Coroutine Keywords
Using any one of these in a function body makes it a coroutine.
- co_await expr- suspend until the awaited operation completes
- co_yield value- suspend and produce a value to the caller (generators)
- co_return value- complete the coroutine, optionally with a value
- promise_type- nested type on the return object that controls coroutine behavior
- coroutine_handle<Promise>- a handle used to resume/destroy a suspended coroutine
A Minimal Generator Type
Bare-metal generator using co_yield (C++20; std::generator exists in C++23's <generator>).
#include <coroutine>#include <optional>template <typename T>struct Generator { struct promise_type { T current_value; Generator get_return_object() { return Generator{ std::coroutine_handle<promise_type>::from_promise(*this) }; } std::suspend_always initial_suspend() { return {}; } std::suspend_always final_suspend() noexcept { return {}; } std::suspend_always yield_value(T value) { current_value = value; return {}; } void return_void() {} void unhandled_exception() { std::terminate(); } }; std::coroutine_handle<promise_type> handle; explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {} ~Generator() { if (handle) handle.destroy(); } bool next() { handle.resume(); return !handle.done(); } T value() { return handle.promise().current_value; }};Generator<int> counter(int start) { for (int i = start;; ++i) co_yield i;}
std::generator (C++23)
The standard library now ships a ready-made generator type in <generator>.
#include <generator>std::generator<int> range(int start, int end) { for (int i = start; i < end; ++i) { co_yield i; }}int sum = 0;for (int x : range(1, 11)) { sum += x; // sums 1..10, coroutine suspends/resumes each iteration}
A Minimal Awaitable Task
The essential shape of a co_await-able async task type.
template <typename T>struct Task { struct promise_type { T result; Task get_return_object() { return Task{ std::coroutine_handle<promise_type>::from_promise(*this) }; } std::suspend_never initial_suspend() { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void return_value(T v) { result = v; } void unhandled_exception() { std::terminate(); } }; std::coroutine_handle<promise_type> handle; // Making Task itself awaitable bool await_ready() { return handle.done(); } void await_suspend(std::coroutine_handle<> caller) { /* schedule resumption */ } T await_resume() { return handle.promise().result; }};Task<int> compute() { co_return 42;}
The Awaiter Concept in Full
The three customization points a type must implement to be co_await-able.
struct LoggingAwaiter { bool await_ready() const noexcept { // true => skip suspension entirely (value already available) // false => suspend and call await_suspend return false; } void await_suspend(std::coroutine_handle<> caller) const { // Called immediately after suspension. Three legal return types: // - void: always suspend, resumption is someone else's job // - bool: true = stay suspended, false = resume caller immediately // - coroutine_handle<>: symmetric transfer (see below) std::cout << "suspending " << caller.address() << "\n"; } int await_resume() const noexcept { // Called on resumption; its return value is the result of `co_await expr` return 42; }};// Usage inside any coroutine body:Task<void> demo() { int v = co_await LoggingAwaiter{};}
Symmetric Transfer to Avoid Stack Growth
Returning a coroutine_handle<> from await_suspend resumes it via a tail call instead of recursive resume(), keeping stack usage O(1) for chained coroutines.
struct FinalAwaiter { bool await_ready() noexcept { return false; } // Resume the continuation directly instead of calling handle.resume() // recursively from inside the current frame -- the compiler compiles // this into a tail call, so a long chain of co_await'd tasks never // blows the stack. std::coroutine_handle<> await_suspend(std::coroutine_handle<promise_type> h) noexcept { auto continuation = h.promise().continuation; return continuation ? continuation : std::noop_coroutine(); } void await_resume() noexcept {}};// promise_type::final_suspend returns FinalAwaiter{} instead of// std::suspend_always{} to chain straight into the awaiting coroutine.
Propagating Exceptions Out of a Coroutine
unhandled_exception() only captures the exception; you must rethrow it somewhere the caller will observe, typically in await_resume().
struct promise_type { std::exception_ptr error; T result; void unhandled_exception() { error = std::current_exception(); } void return_value(T v) { result = std::move(v); } // Awaiting this Task rethrows on the caller's stack, not the callee's struct Awaiter { std::coroutine_handle<promise_type> h; bool await_ready() { return false; } std::coroutine_handle<> await_suspend(std::coroutine_handle<> caller) { h.promise().continuation = caller; return h; } T await_resume() { if (h.promise().error) std::rethrow_exception(h.promise().error); return std::move(h.promise().result); } };};
Custom Coroutine Frame Allocation
The compiler-generated heap allocation for a coroutine frame can be overridden per promise_type, and failure handled without exceptions.
struct promise_type { // Overrides operator new used for THIS coroutine's frame only static void* operator new(std::size_t size) { void* p = my_pool_alloc(size); if (!p) throw std::bad_alloc{}; return p; } static void operator delete(void* p, std::size_t size) { my_pool_free(p, size); } // Opt in to noexcept coroutine creation: called instead of throwing // bad_alloc if operator new returns nullptr (requires a nothrow new). static Task get_return_object_on_allocation_failure() { return Task::allocation_failed(); }};// Note: many real compilers elide the heap allocation entirely (HALO --// Heap Allocation eLision Optimization) when they can prove the coroutine's// lifetime is provably nested within the caller's -- but you cannot rely on it.
Coroutine Frame & Machinery Terms
Vocabulary needed to read compiler diagnostics and cppcoro/folly::coro source.
- coroutine frame- compiler-generated heap object holding locals, params, and suspend-point state
- HALO- Heap Allocation eLision Optimization: frame lives on caller's stack when provably safe
- std::noop_coroutine()- a handle representing 'nothing to resume', used as a symmetric-transfer terminator
- suspend point- any co_await/co_yield; state needed to resume is saved into the frame
- initial_suspend / final_suspend- promise hooks controlling eager-vs-lazy start and post-completion behavior
- coroutine_handle<>::done()- true once the coroutine has run past final_suspend
- symmetric transfer- await_suspend returning a handle instead of resuming imperatively, avoiding stack growth
Writing coroutine machinery (promise_type, awaiters) by hand is a deep rabbit hole — for real async code prefer a battle-tested library (cppcoro, folly::coro, or Boost.Cobalt) and reserve hand-rolled promise types for learning or truly bespoke generator/task needs.