Pre-history: asynchronous logic is hand-written state machines and callback chains — the shape coroutines will later generate for you.
struct ReadState {
enum { Start, Waiting, Done } phase = Start;
void on_ready(Bytes b) { }
};
Generic lambdas make callback pyramids slightly less painful; the state-machine pattern is unchanged.
The Coroutines TS ships experimentally (co_await behind flags) — production code mostly watches from the sidelines.
The machinery lands: co_await/co_yield/co_return, promise_type, the awaitable protocol (await_ready/await_suspend/await_resume), symmetric transfer via coroutine_handle to avoid stack growth. Deliberately no standard task or generator — you write the policy.
struct Awaiter {
bool await_ready() const noexcept { return false; }
std::coroutine_handle<>
await_suspend(std::coroutine_handle<> h) { return next(h); }
Bytes await_resume() { return std::move(buf_); }
};
std::generator<T> — the first standard coroutine type: a synchronous, lazy, recursion-friendly range. The reference generator for how promise types should behave (allocator-aware, elements_of for nesting).
std::generator<int> fib() {
for (auto [a, b] = std::pair{0, 1};; std::tie(a, b) = std::pair{b, a + b})
co_yield a;
}
for (int x : fib() | std::views::take(8)) use(x);
std::execution::task (adopted late in the 26 cycle, P3552) gives coroutines a standard lazy task that plugs into senders/receivers — scheduler affinity and cancellation handled by the framework, not your promise type.
std::execution::task<int> load(Id id) {
auto bytes = co_await fetch(id);
co_return parse(bytes);
}
C++29direction · not adopted
Direction: async generators, guaranteed allocation elision, and deeper coroutine/sender unification are the open threads.