Coroutines
|
This section documents C++23 (ISO/IEC 14882:2024), as published by ISO/IEC JTC1/SC22/WG21 (wg21), verified against the freely available working draft N5046 (eel.is/c++draft) and cppreference.com. This content was generated with the assistance of AI and should be verified against the working draft and cppreference.com before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
A C++20 coroutine is an ordinary-looking function that uses co_await, co_yield, or co_return — their mere
presence makes the whole function a coroutine, compiled into a state machine that can suspend and later
resume exactly where it left off, instead of running start-to-finish in one go.
co_await, co_yield, co_return
-
co_await expr— suspend untilexpr(an awaitable) says it’s ready, then resume with its result. -
co_yield value— produce one value to the caller and suspend, resuming on the next request (drives generators). -
co_return value;(or bareco_return;) — finish the coroutine, deliveringvalueto whatever is consuming it.
Unlike an ordinary return, none of these three unwind the coroutine’s local state — that’s exactly what lets
it resume later with everything still intact.
Promise Types and Awaitables
The compiler doesn’t know what "suspend" or "produce a result" mean for your coroutine on its own — a
promise type (unrelated to std::promise) spells that out, and an awaitable (anything with
await_ready/await_suspend/await_resume) spells out what a single co_await does:
#include <coroutine>
struct MinimalPromise {
// required promise-type interface:
auto get_return_object() { return std::coroutine_handle<MinimalPromise>::from_promise(*this); }
std::suspend_always initial_suspend() { return {}; } // suspend immediately -- caller decides when to start
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); } // simplest possible policy: give up on an exception
};
std::suspend_always/std::suspend_never are the two trivial, standard-provided awaitables (always suspend /
never suspend) — most real coroutine types (below) define their own richer ones.
A task Type
A minimal, fire-and-run "start a coroutine, don’t wait for a result" type built from the pieces above:
#include <coroutine>
#include <iostream>
struct Task {
struct promise_type {
Task get_return_object() { return Task{std::coroutine_handle<promise_type>::from_promise(*this)}; }
std::suspend_never initial_suspend() { return {}; } // start running immediately
std::suspend_always final_suspend() noexcept { return {}; } // stay suspended at the end -- lets
// Task's destructor own destroying the
void return_void() {} // frame; suspend_never here would
void unhandled_exception() { std::terminate(); } // auto-destroy it, and ~Task's
}; // handle.destroy() would then be
// a double-free
std::coroutine_handle<promise_type> handle;
~Task() { if (handle) handle.destroy(); }
};
Task greet() {
std::cout << "before suspend\n";
co_await std::suspend_always{}; // suspends here -- something else would need to call handle.resume()
std::cout << "after resume\n"; // to reach this line
}
A generator Type
A minimal, from-scratch lazy generator using co_yield — what a hand-written generator needs to implement,
before reaching for the standard one below:
#include <coroutine>
#include <optional>
template <typename T>
struct Generator {
struct promise_type {
T currentValue;
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) { // called for every "co_yield value"
currentValue = 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() {
if (handle.done()) return false;
handle.resume();
return !handle.done();
}
T value() const { return handle.promise().currentValue; }
};
Generator<int> countUpTo(int n) {
for (int i = 1; i <= n; ++i) {
co_yield i;
}
}
// Generator<int> g = countUpTo(3);
// while (g.next()) { use(g.value()); } // 1, 2, 3
std::generator (C++23)
C++23 standardizes exactly this pattern as std::generator<T>, eliminating the promise-type boilerplate above
for the common case:
#include <generator>
std::generator<int> countUpTo(int n) {
for (int i = 1; i <= n; ++i) {
co_yield i;
}
}
// for (int value : countUpTo(3)) { use(value); } // works directly with range-for -- 1, 2, 3
<generator> is standard-conformant C++23 (verified against cppreference and the working draft) but is
not yet shipped by this environment’s libstdc++ 13 — it needs GCC 14+, a recent libc++, or MSVC (see
Getting Started). The hand-written Task/Generator types
above do compile locally, and demonstrate the exact mechanism std::generator packages.
|
Suspend/Resume States
suspend_always Created --> Running: initial_suspend() returns
suspend_never Suspended --> Running: handle.resume() called Running --> Suspended: co_await / co_yield
suspends again Running --> Done: co_return, or falls
off the end Done --> [*]: handle.destroy()