Coroutines (C++20)
C++20 coroutines — suspendable functions built on co_await, co_yield, and co_return with a low-level promise/awaitable customization framework.
Coroutinessince C++20A coroutine is a function that can suspend its execution at defined points and resume later while preserving all local state, implemented via the keywords co_await, co_yield, and co_return and a compiler-generated heap-allocated state machine.
Overview
C++20 ships coroutines as a framework, not a set of ready-to-use types. The standard provides the machinery — keywords, std::coroutine_handle<>, std::suspend_always, std::suspend_never in <coroutine> — but delegates high-level types (tasks, generators, schedulers) to libraries or later standards. C++23 delivers the first concrete coroutine type: std::generator<T> in <generator>. A standard async task type (std::execution) is targeting C++26.
Any function containing co_await, co_yield, or co_return is a coroutine. The compiler transforms it into a state machine capturing all local variables and the current suspension point in a coroutine frame — a heap-allocated object managed via std::coroutine_handle<Promise>.
Three actors collaborate on every coroutine invocation:
- Promise object — lives inside the frame; the compiler calls its methods to handle
co_yield,co_return, exceptions, and initial/final suspension. - Coroutine handle (
std::coroutine_handle<Promise>, C++20) — a thin non-owning pointer to the frame;.resume()continues from the last suspension point,.destroy()deallocates the frame. - Awaitable — any object with
await_ready(),await_suspend(), andawait_resume();co_await exprobtains an awaitable fromexpr(viaoperator co_awaitif needed) and drives this three-method protocol.
Syntax
co_yield value; // C++20: produce a value to the caller and suspend
co_await expr; // C++20: suspend until expr's awaitable signals readiness
co_return; // C++20: complete with no value (calls promise.return_void())
co_return value; // C++20: complete with a value (calls promise.return_value(value))The awaitable protocol (C++20)
struct MyAwaitable {
// Return true to skip suspension entirely (fast path)
bool await_ready() const noexcept;
// Called just before suspension. Three legal return types:
// void — unconditionally suspend; caller drives wakeup
// bool — false = suspend, true = resume immediately (no suspend)
// coroutine_handle<> — symmetric transfer: tail-resume that coroutine
void await_suspend(std::coroutine_handle<> h);
// Called on resume; its return value becomes the result of the co_await expression
ReturnType await_resume();
};Minimal promise type (C++20)
struct MyPromise {
MyCoroutine get_return_object(); // builds the wrapper returned to the caller
std::suspend_never initial_suspend(); // suspend_never = start running immediately
std::suspend_always final_suspend() noexcept; // noexcept required; keeps frame alive for result pickup
void unhandled_exception() noexcept; // called if the body throws
void return_void(); // for bare co_return;
// — OR —
void return_value(T v); // for co_return value;
std::suspend_always yield_value(T v); // for co_yield value;
};final_suspend() must be noexcept. Returning suspend_always keeps the frame alive so the caller can extract a result before destruction; suspend_never destroys it immediately.
Examples
std::generator — lazy sequences (C++23)
std::generator<T> is the first standard coroutine type, providing a synchronous pull-model generator defined in <generator> and standardised in C++23. It implements std::ranges::input_range, so it composes directly with range adaptors.
#include <generator> // C++23
#include <ranges>
#include <print> // C++23
std::generator<int> fibonacci() {
int a = 0, b = 1;
while (true) {
co_yield a;
auto next = a + b;
a = b;
b = next;
}
}
int main() {
for (int n : fibonacci() | std::views::take(10)) {
std::print("{} ", n); // 0 1 1 2 3 5 8 13 21 34
}
}std::generator<int> is move-only and lazily resumes on each iterator increment. The coroutine frame is destroyed when the generator object goes out of scope. The infinite loop is safe because std::views::take(10) stops pulling after ten values.
Yielding by reference with std::generator<T&>
When each yielded value is large or you want the consumer to observe mutations, use a reference specialisation. The generator yields references to elements inside the coroutine's own storage.
#include <generator> // C++23
#include <string>
#include <vector>
std::generator<std::string&> owned_strings(std::vector<std::string> strs) {
// strs lives inside the coroutine frame — safe to yield by reference
for (auto& s : strs) {
co_yield s;
}
}
// Consumer can modify the yielded values in place
for (std::string& s : owned_strings({"hello", "world"})) {
s += "!";
}Best Practice. Use std::generator<const T&> when yielding elements of a container that lives inside the frame. Use std::generator<T> (by value) when the yielded value is cheap to copy or when lifetime is uncertain.
Recursive generators with elements_of (C++23)
Recursive traversal using std::ranges::elements_of (C++23) avoids O(depth) stacked frames:
struct Node { int value; Node* left{}; Node* right{}; };
std::generator<int> in_order(const Node* node) {
if (!node) co_return;
co_yield std::ranges::elements_of(in_order(node->left)); // C++23: flatten sub-generator
co_yield node->value;
co_yield std::ranges::elements_of(in_order(node->right));
}Without elements_of, a naive for (int x : sub) co_yield x; allocates a new frame per recursive call, producing O(N) stack depth on an unbalanced tree. elements_of delegates iteration via symmetric transfer, keeping stack depth constant.
Building a minimal Task<T> (C++20)
A Task<T> is a value type that represents the result of an asynchronous computation. The example below is synchronous and single-shot but demonstrates the full promise protocol.
#include <coroutine> // C++20
#include <optional>
#include <exception>
#include <stdexcept>
template<typename T>
struct Task {
struct Promise {
std::optional<T> result;
std::exception_ptr exc;
// Called first — build the Task wrapper that the caller receives
Task get_return_object() {
return Task{std::coroutine_handle<Promise>::from_promise(*this)};
}
// Suspend before executing the body; caller must call .resume() explicitly
std::suspend_always initial_suspend() noexcept { return {}; }
// Keep the frame alive after the body finishes so .get() can read the result
std::suspend_always final_suspend() noexcept { return {}; }
// co_return value; stores the result
void return_value(T v) { result = std::move(v); }
// Any uncaught exception is captured here; rethrown by .get()
void unhandled_exception() noexcept {
exc = std::current_exception();
}
};
using promise_type = Promise;
explicit Task(std::coroutine_handle<Promise> h) : handle_(h) {}
// Destructor releases the frame
~Task() { if (handle_) handle_.destroy(); }
// Non-copyable, movable
Task(const Task&) = delete;
Task& operator=(const Task&) = delete;
Task(Task&& o) noexcept : handle_(std::exchange(o.handle_, {})) {}
// Drive the coroutine to completion and return its result (or rethrow)
T get() {
handle_.resume();
auto& p = handle_.promise();
if (p.exc) std::rethrow_exception(p.exc);
return std::move(*p.result);
}
private:
std::coroutine_handle<Promise> handle_;
};
Task<int> square(int x) {
co_return x * x; // calls promise.return_value(x * x)
}
Task<double> hypotenuse(double a, double b) {
co_return std::sqrt(a * a + b * b);
}
// Usage:
// assert(square(9).get() == 81);
// assert(hypotenuse(3, 4).get() == 5.0);initial_suspend() returning suspend_always means the body does not execute until the caller invokes .resume() (which .get() does). This is the lazy model preferred for async tasks — it avoids running work before the caller has set up completion callbacks or an executor.
Making Task awaitable
To use co_await task inside another coroutine, add operator co_await to Task:
// Inside Task<T>:
auto operator co_await() noexcept {
struct Awaiter {
std::coroutine_handle<Promise> inner;
bool await_ready() noexcept {
inner.resume(); // drive inner coroutine to completion
return true; // resume caller immediately (synchronous path)
}
void await_suspend(std::coroutine_handle<>) noexcept {}
T await_resume() {
auto& p = inner.promise();
if (p.exc) std::rethrow_exception(p.exc);
return std::move(*p.result);
}
};
return Awaiter{handle_};
}
// Now usable as:
Task<int> compute() {
int s = co_await square(7); // 49
co_return s + 1; // 50
}This synchronous awaiter is for illustration. A real async task would not call inner.resume() in await_ready; instead it would register a callback so that inner resumes the outer coroutine when it finishes.
co_await with standard awaitables (C++20)
The standard provides two trivial awaitables that let you control suspension from inside a coroutine body without writing a custom type:
#include <coroutine> // C++20
Task<void> demo_suspension() {
// Always suspends: returns control to the caller/scheduler immediately.
// The coroutine will not continue until something calls handle.resume().
co_await std::suspend_always{};
// Never suspends: the expression is a no-op; the coroutine keeps running.
// Useful for conditional paths where you sometimes want to yield.
co_await std::suspend_never{};
}std::suspend_always is the canonical "yield point" for cooperative schedulers: a scheduler calls .resume() on the handle when it grants the coroutine its next time slice.
Getting a handle from inside the promise
std::coroutine_handle<Promise>::from_promise(p) reconstructs a handle from a reference to the promise object. This is how get_return_object() manufactures the Task wrapper, and it is also useful for self-scheduling:
struct SelfSchedulingPromise {
// ...
std::suspend_always initial_suspend() noexcept { return {}; }
// In the promise, get the handle for this coroutine:
auto get_handle() {
return std::coroutine_handle<SelfSchedulingPromise>::from_promise(*this);
}
};Inside the coroutine body you can also capture the handle via a custom awaitable whose await_suspend saves the handle to an external scheduler queue.
Symmetric transfer (C++20)
Symmetric transfer allows await_suspend to return a std::coroutine_handle<>, which the runtime will resume immediately as a tail call rather than returning up the call stack. Without it, chaining N awaiting coroutines consumes O(N) stack frames.
Consider an async chain where task C awaits task B which awaits task A:
// WITHOUT symmetric transfer — O(N) stack depth
// resume(A) → resume(B) → resume(C) → returns
// each resume() pushes a new stack frame
// WITH symmetric transfer — O(1) stack depth
struct SymmetricAwaiter {
std::coroutine_handle<> continuation; // the coroutine waiting on us
bool await_ready() noexcept { return false; }
// Return the continuation handle — runtime performs a tail resume,
// suspending THIS coroutine and activating the continuation without
// deepening the stack.
std::coroutine_handle<> await_suspend(
std::coroutine_handle<> /* current */) noexcept
{
return continuation; // symmetric transfer to continuation
}
void await_resume() noexcept {}
};A practical Task that chains correctly stores the caller's handle in the promise and uses symmetric transfer in final_suspend:
template<typename T>
struct ChainableTask {
struct Promise {
std::coroutine_handle<> continuation{std::noop_coroutine()};
std::optional<T> result;
// Custom final awaitable that transfers to the continuation
struct FinalAwaiter {
bool await_ready() noexcept { return false; }
std::coroutine_handle<> await_suspend(
std::coroutine_handle<Promise> h) noexcept
{
// Symmetric transfer: activate the waiting coroutine
// without growing the stack
return h.promise().continuation;
}
void await_resume() noexcept {}
};
ChainableTask get_return_object();
std::suspend_always initial_suspend() noexcept { return {}; }
FinalAwaiter final_suspend() noexcept { return {}; }
void return_value(T v) { result = std::move(v); }
void unhandled_exception() noexcept { /* capture exc */ }
};
using promise_type = Promise;
// ... operator co_await stores handle_.promise().continuation = caller_handle
};Best Practice. Any coroutine framework that chains tasks must use symmetric transfer in final_suspend. Failing to do so causes stack overflows on long async chains — a bug that only manifests under load.
Coroutine heap allocation elision (HALO)
When the compiler can prove that a coroutine's lifetime is strictly nested within the caller's stack frame — the coroutine is created, fully awaited, and destroyed before the calling function returns — it may elide the heap allocation for the coroutine frame entirely and allocate the frame on the caller's stack instead.
// HALO-eligible: compiler can see the entire lifetime of inner()
int outer() {
auto t = inner(); // coroutine frame might be stack-allocated
t.get(); // driven to completion inside outer()
return 0; // frame destroyed before outer() returns
} // — no heap allocation requiredHALO does not apply when:
- The coroutine handle escapes to a different translation unit (the compiler cannot see the full lifetime)
- The coroutine is stored in a container or passed across a virtual dispatch boundary
- The frame size is too large for the compiler's heuristic to risk stack allocation
- The coroutine is conditionally driven (e.g., stored in
std::optionalthat might not be engaged)
Best Practice. Do not rely on HALO in performance-critical code without profiling. Use compiler explorer (-O2 -fno-exceptions) to check whether the allocation is elided. The standard does not mandate HALO; it is a quality-of-implementation optimisation (allowed under the as-if rule).
Custom awaitable — off-thread resumption (C++20)
#include <coroutine> // C++20
#include <thread>
struct ResumeOnThread {
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) {
// Transfer ownership of the handle to the new thread
std::thread([h]() mutable { h.resume(); }).detach();
}
void await_resume() const noexcept {}
};
// co_await ResumeOnThread{};
// — execution from here runs on a detached threadException handling in coroutines (C++20)
When an exception escapes the coroutine body uncaught, the runtime calls promise_type::unhandled_exception() instead of propagating the exception directly to the caller. This gives the promise full control over what happens to the error.
#include <coroutine> // C++20
#include <exception>
#include <stdexcept>
template<typename T>
struct SafeTask {
struct Promise {
std::optional<T> result;
std::exception_ptr exc; // captured here by unhandled_exception()
SafeTask get_return_object() {
return SafeTask{std::coroutine_handle<Promise>::from_promise(*this)};
}
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_value(T v) { result = std::move(v); }
// Called by the runtime when an exception escapes the body
void unhandled_exception() noexcept {
exc = std::current_exception(); // capture; do NOT rethrow here
}
};
using promise_type = Promise;
explicit SafeTask(std::coroutine_handle<Promise> h) : handle_(h) {}
~SafeTask() { if (handle_) handle_.destroy(); }
T get() {
handle_.resume();
auto& p = handle_.promise();
if (p.exc) {
std::rethrow_exception(p.exc); // rethrow at the call site
}
return std::move(*p.result);
}
private:
std::coroutine_handle<Promise> handle_;
};
SafeTask<int> might_throw(bool fail) {
if (fail) throw std::runtime_error("coroutine failed");
co_return 42;
}
// try { might_throw(true).get(); }
// catch (const std::runtime_error& e) { /* caught here */ }The key rules are:
unhandled_exception()must not itself throw (mark itnoexceptor handle exceptions internally).- Do not call
std::terminate()silently — always capture withstd::current_exception()and rethrow at the awaiter. - If
unhandled_exception()is not defined, the program callsstd::terminate()on an uncaught exception.
Common Pitfall. If you forget unhandled_exception() in your promise type, any exception from the coroutine body calls std::terminate() with no diagnostic. Always define it, even if only to call std::terminate() explicitly with a message.
for co_await — async range iteration
for co_await is a common pattern in async codebases (notably Asio and cppcoro) for iterating over an async sequence. It is not yet part of the standard (as of C++23) but is widely supported as a library-level abstraction:
// Conceptual pattern — not standard syntax, library-provided
// async_range<int> is a coroutine-backed input range
// Expands approximately to:
auto it = co_await range.begin();
auto end = range.end();
for (; it != end; co_await ++it) {
int value = *it;
// process value...
}With std::generator (C++23) the range is synchronous, so ordinary for suffices. The for co_await pattern matters only when each step of the iteration is itself an async operation — for example, reading successive chunks from a network socket.
Best Practices
Use library types for application code. Implement the promise interface only when building coroutine infrastructure. Reach for std::generator (C++23), Asio's awaitable<T>, or cppcoro's task<T> for everything else.
Let final_suspend return suspend_always for result-bearing coroutines. This keeps the frame alive long enough for the caller to read the value before destroy() is called.
Use symmetric transfer to chain async coroutines without stack growth. When await_suspend returns a coroutine_handle<>, the runtime performs a tail-call-like transfer — the stack does not deepen. This is critical for deeply nested co_await chains.
Pass inputs by value when coroutine lifetime may outlive the caller. Any reference argument must remain valid across all suspension points, which is difficult to guarantee at call sites.
Profile before assuming HALO. Compilers may elide the heap allocation when a coroutine's lifetime is provably nested in the caller (Heap Allocation eLision Optimisation), but it is not mandated. For performance-critical paths, measure.
Always define unhandled_exception(). A missing unhandled_exception causes std::terminate() on any uncaught exception from the body. Capture with std::current_exception() and rethrow at the point where the result is consumed.
Common Pitfalls
Dangling references across suspension
// DANGER: reference to local/stack variable
Task<int> bad(const std::vector<int>& vec) {
co_await some_async_op();
co_return vec[0]; // vec may be destroyed — UB
}
// Fix: capture by value
Task<int> good(std::vector<int> vec) {
co_await some_async_op();
co_return vec[0];
}The coroutine frame outlives the original call expression. The compiler cannot diagnose this; ASAN/UBSAN may catch it at runtime.
Forgetting co_await (getting the awaitable instead of its result)
// BUG: missing co_await
Task<int> compute_value();
Task<void> consumer() {
auto t = compute_value(); // t is a Task<int>, NOT an int
// t is an unawaited task — the inner coroutine may never run
// and the result is never extracted
// Fix:
int result = co_await compute_value();
}Common Pitfall. The compiler does not warn about an unused awaitable in most cases. The coroutine runs (or does not run, depending on initial_suspend) and its result is silently discarded when t goes out of scope. Enable -Wunused-result and similar warnings, and prefer types that [[nodiscard]]-mark their co_await operator.
Destroying a coroutine handle at final_suspend
// DANGER: double-destroy
void bad_cleanup(std::coroutine_handle<> h) {
// If h is suspended at final_suspend (a normal, valid state),
// calling .destroy() here is safe — that is the correct cleanup path.
// BUT: if the coroutine has already been destroyed elsewhere, this is UB.
h.destroy(); // only safe if you uniquely own h and it has not been destroyed
}
// Safety rule: treat coroutine_handle like a raw pointer.
// Use a single owning wrapper (like Task) that calls destroy() in its destructor
// and is non-copyable to prevent double-destroy.A coroutine suspended at final_suspend must eventually be destroyed via handle.destroy(). Destroying it twice, or destroying it while it is running (i.e., between resume() and the next suspension), is undefined behaviour.
No executor means no resumption
co_await suspends the coroutine, not the thread. Without a scheduler calling handle.resume() when the awaitable completes, the coroutine hangs silently. Always embed co_await-based code inside a runtime that drives resumption (Asio io_context, a thread pool, etc.).
final_suspend must be noexcept
If final_suspend() itself throws, or if the awaitable it returns throws during await_suspend/await_resume, the behaviour is undefined. Keep final-suspend awaitables trivial and unconditionally noexcept.
Forgetting promise_type
The compiler looks for coroutine_traits<ReturnType, Args...>::promise_type. The standard path is a nested using promise_type = ... inside the return type. Omitting it produces a hard-to-diagnose error along the lines of 'promise_type' is not a member of 'std::coroutine_traits<...>'.
No standard async task in C++20 or C++23
std::generator (C++23) covers synchronous generators. There is no std::task<T> in C++20 or C++23. std::execution (P2300, targeting C++26) will provide senders, receivers, and a scheduler model. Until then, use Asio, cppcoro, or libunifex for production async coroutines.
Quick Reference
| Feature | Header | Since |
|---|---|---|
co_await, co_yield, co_return keywords | (built-in) | C++20 |
std::coroutine_handle<P> | <coroutine> | C++20 |
std::suspend_always, std::suspend_never | <coroutine> | C++20 |
std::noop_coroutine() | <coroutine> | C++20 |
std::generator<T> | <generator> | C++23 |
std::ranges::elements_of | <ranges> | C++23 |
std::execution (senders/receivers) | <execution> | C++26 (planned) |
See Also
- Ranges — compose
std::generatorwithstd::viewspipelines - Concepts — constrain coroutine return types and awaitables
- Move Semantics — coroutine frames use move construction for value types
<coroutine>reference —coroutine_handle,suspend_always,suspend_never<generator>reference —std::generator<T>, C++23