C++ Coroutines (C++20)
- Description: A note on C++20 coroutines —
co_await,co_yield,co_return,std::generator(C++23), custom awaitables, common coroutine types, and when to use them - My Notion Note ID: K2A-B1-26
- Created: 2021-08-01
- Updated: 2026-02-28
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. What's a Coroutine?
- 2. The Three Operators:
co_await,co_yield,co_return - 3.
std::generator(C++23) - 4. Coroutine Anatomy: Promise and Awaitable
- 5. Common Coroutine Types
- 6. When to Use Coroutines
- 7. Performance Considerations
1. What's a Coroutine?
- Coroutine = function that can be suspended and resumed.
- Looks like an ordinary call to the caller; body can yield control midway, save state, resume later (possibly different thread, possibly post-async-op).
- Function is a coroutine iff body uses
co_await,co_yield, orco_return.
#include <generator> // C++23
std::generator<int> count_to(int n) {
for (int i = 0; i < n; ++i) {
co_yield i; // suspend, hand i back to caller, resume later
}
}
for (int x : count_to(5)) {
std::cout << x << " "; // 0 1 2 3 4
}
2. The Three Operators: co_await, co_yield, co_return
| Operator | Meaning |
|---|---|
co_await expr |
Suspend the coroutine until expr is "ready"; produce a value when resumed. |
co_yield val |
Suspend the coroutine, giving val to the caller. Equivalent to co_await yield_value(val). |
co_return val |
End the coroutine, optionally producing a final value. |
- Any combination usable in one coroutine body.
task<int> compute() {
int a = co_await fetch_a(); // suspend, resume when fetch_a is done
int b = co_await fetch_b();
co_return a + b;
}
3. std::generator (C++23)
std::generator<T>— standard coroutine type producing a sequence ofTs. Is astd::ranges::range→ works with ranges.
#include <generator>
#include <ranges>
std::generator<int> fibs() {
int a = 0, b = 1;
while (true) {
co_yield a;
int next = a + b;
a = b;
b = next;
}
}
for (int x : fibs() | std::views::take(10)) std::cout << x << " ";
// 0 1 1 2 3 5 8 13 21 34
- Easiest coroutine type — synchronous, pull-based (caller drives), no thread-safety concerns.
4. Coroutine Anatomy: Promise and Awaitable
- Library code, not language magic. 2 types do the work:
- Promise type — defined inside return type. Controls what coroutine produces + lifetime (e.g.,
std::generator<T>::promise_type). - Awaitable — what
co_await exproperates on. Hasawait_ready,await_suspend,await_resume.
- Rarely write either yourself unless implementing a custom coroutine type. Use
std::generator(C++23) or cppcoro / asio coroutines for async I/O.
Minimal awaitable:
struct Suspend {
bool await_ready() const noexcept { return false; } // suspend?
void await_suspend(std::coroutine_handle<>) const noexcept {} // what to do on suspend
void await_resume() const noexcept {} // value on resume
};
- stdlib provides
std::suspend_alwaysandstd::suspend_neverfor trivial cases.
5. Common Coroutine Types
- C++20 = language facility only. stdlib doesn't ship a full set of coroutine types yet.
std::generator<T>(C++23) — synchronous pull generator. stdlib.task<T>— async return value. Not standard yet (expected C++26). Use cppcoro, folly, or asio.lazy<T>,shared_task<T>, async streams — third-party.
- Until C++26: third-party coroutine lib (or framework-provided — Boost.Asio's
awaitable<T>, Qt'sQCoro).
6. When to Use Coroutines
- Generators / lazy sequences — cleaner than custom iterators or callback pipelines.
- Async I/O — straight-line code, event loop behind the scenes.
- State machines — each
co_await= state transition; language handles bookkeeping. - Pull-based parsing — stream tokens/AST nodes without materializing whole input.
When not:
- Hot loops — suspension overhead matters.
- Already-simple sync code — coroutine adds return type, promise type, lifetime concerns.
- Tight memory budgets — each frame heap-allocated by default (compiler may elide, not always).
7. Performance Considerations
- Heap allocation per coroutine — compiler may elide (HALO — Heap Allocation eLision Optimization), but only if it can prove frame doesn't escape. Don't rely on it for hot paths.
- State-machine overhead — each suspension = switch dispatch. Negligible for coarse-grained async, noticeable in tight loops.
- Inlining — harder for compiler to inline through.
- Symmetric transfer — coroutine awaiting another switches directly without unwinding stack. Efficient.
- For most async workloads: net win in readability, comparable perf to hand-written callback chains.
- For generators: similar to well-written iterator.