Coroutines (C++20)
A coroutine is a function that can suspend itself, hand control back to its caller, and later resume exactly where it left off โ with all its local state intact. That makes two things natural that ordinary functions can't express: lazy sequences (generators) and asynchronous code that reads like synchronous code.
Any function is a coroutine if its body uses one of three keywords:
| Keyword | Meaning |
|---|---|
co_await | suspend until an awaited operation is ready, then resume |
co_yield | produce a value and suspend (the generator pattern) |
co_return | finish the coroutine, optionally with a value |
C++20 standardised the low-level machinery (std::coroutine_handle, the promise_type
protocol) but almost no ready-to-use types. A usable std::generator arrives in C++23; task
and async types still come from libraries (cppcoro, Asio, libunifex) or your own glue. Expect to
either use a library or write a small amount of boilerplate.
Suspend and resumeโ
Unlike a normal call that runs to completion, a coroutine bounces control back and forth with its caller. Its state lives in a heap-allocated coroutine frame rather than on the stack (coroutines are stackless), which is why locals survive across a suspension.
Generators โ the intuitive case (co_yield)โ
A generator computes values lazily: each co_yield produces one element and suspends until the
consumer asks for the next. The sequence can even be infinite, because nothing is computed ahead of
demand.
#include <generator> // C++23
std::generator<int> fibonacci() {
int a = 0, b = 1;
while (true) { // infinite โ only as many are produced as are pulled
co_yield a; // hand back one value, suspend here
a = std::exchange(a, b), b = a + b;
}
}
int main() {
for (int x : fibonacci()) { // each iteration resumes the coroutine
if (x > 50) break;
std::print("{} ", x); // 0 1 1 2 3 5 8 13 21 34
}
}
This is the same lazy spirit as Ranges โ and generators compose with range adaptors.
Async โ synchronous-looking concurrency (co_await)โ
co_await suspends the coroutine until an operation completes, then resumes โ without blocking the
thread. The code reads top-to-bottom even though it yields at each await:
// 'task<>' here is a library/utility type, not standard in C++20.
task<std::string> fetch_user(int id) {
auto conn = co_await connect(); // suspend until connected
auto row = co_await conn.query(id); // suspend until the query returns
co_return row.name; // produce the result
}
Compared with futures and promises or raw threads, coroutines avoid callback nesting and let one thread juggle many in-flight operations.
The machinery, brieflyโ
When you write a coroutine, the compiler rewrites it around two cooperating pieces:
promise_typeโ found via the return type; its hooks (get_return_object,initial_suspend,yield_value,return_value,final_suspend) decide what the coroutine returns and how it suspends. This is the boilerplate libraries provide for you.- Awaiter โ what
co_await exprdrives:await_ready()(skip suspension if already done),await_suspend(handle)(schedule the resume),await_resume()(produce the awaited value).
You rarely write this by hand once you have a generator/task type โ but knowing it exists explains the error messages.
A coroutine's parameters are copied into the frame, but references and views are copied as
references โ if the referent dies during a suspension, you have a dangling reference. Be especially
careful passing std::string_view, span, or const T& into a coroutine that suspends.
task<void> log(const std::string& msg); // risky if caller's string dies while suspended
task<void> log(std::string msg); // safer: own the data in the frame
Summaryโ
- A coroutine suspends and resumes, preserving locals in a heap frame (stackless).
co_yieldbuilds lazy/infinite generators;co_awaitbuilds non-blocking async flows;co_returnfinishes.- C++20 provides only the low-level protocol;
std::generatoris C++23, andtask/async types come from libraries. - The
promise_type+ awaiter protocol is the plumbing the compiler generates around your code. - Watch for dangling reference parameters across suspension points โ prefer owning the data.
Relatedโ
- Futures and Promises ยท Threads ยท Thread Pools โ other concurrency models
- Ranges โ the same lazy-sequence idea
- C++ Versions โ coroutines (C++20),
std::generator(C++23)