Coroutines
A coroutine is a function that can pause mid-execution, hand control back to whoever called it, and later pick up exactly where it left off — locals, position, and all — which is the mechanism behind generators and much of modern async code.
An ordinary function has a simple lifetime: it is called, it runs to completion (or throws), and its stack frame disappears the instant it returns. A coroutine, introduced as a language feature in C++20, breaks that lifetime apart. Writing co_await, co_yield, or co_return anywhere in a function's body turns that function into a coroutine, and the compiler transforms it into a state machine backed by a heap-allocated (usually) frame that can suspend execution at defined points and resume it later, with every local variable still holding the value it had when the coroutine paused. That single capability — suspend now, resume later, state preserved — is what makes coroutines the natural way to write generators that produce values lazily and asynchronous code that reads like ordinary sequential logic instead of a tangle of callbacks. This page builds the machinery up from the three keywords to a working generator and a minimal async-style task type.
What Makes a Function a Coroutine
There is no coroutine keyword in C++. Instead, a function becomes a coroutine purely by using co_return, co_yield, or co_await anywhere in its body. The compiler detects this at compile time and generates completely different machine code for that function than it would for an ordinary one — code that can pause and later resume instead of running start to finish in one go.
The presence of co_yield is what makes count_up a coroutine, not its return type on its own. A coroutine's return type must be a type that satisfies certain compiler-checked requirements — informally, it needs a nested promise_type (or a specialization of std::coroutine_traits) that defines how suspension, resumption, and the return value are handled.
A function cannot be both a coroutine and certain other things at once: main() can never be a coroutine, and neither can constexpr functions, functions using variadic arguments (...), or constructors and destructors.
#include <coroutine>
// Using co_yield anywhere in this body makes count_up a coroutine.
// Its actual return type has to satisfy the coroutine requirements —
// see the Generator type built later on this page.
Generator<int> count_up(int start, int end)
{
for (int i = start; i <= end; ++i)
co_yield i;
}The Coroutine Frame
When a coroutine is called, the compiler-generated code allocates a coroutine frame — typically on the heap, though the compiler is permitted to elide that allocation when it can prove the frame's lifetime is safely bounded. The frame holds everything the coroutine needs to remember across a suspension: the promise object, copies of the coroutine's parameters, every local variable whose lifetime spans a suspension point, and a record of exactly where execution should resume.
This is the entire reason coroutines can do what ordinary functions cannot. When count_up above hits co_yield i, the frame — including the value of i and the loop's position — survives even though execution has returned control to whoever called count_up. Calling an ordinary function again from scratch would reset i to start; resuming a suspended coroutine picks the loop up exactly where it paused.
co_return, co_yield, and co_await
Three keywords cover everything a coroutine body can do. co_return ends the coroutine, analogous to return in an ordinary function — it calls promise.return_void() for a coroutine with no meaningful result, or promise.return_value(v) to hand back a value. co_yield expr suspends the coroutine and produces a value to whoever is consuming it; it is defined in terms of co_await, expanding to co_await promise.yield_value(expr). co_await expr suspends until the awaited operation signals it is ready, and it is the most general of the three — both co_yield and the implicit suspensions in a coroutine's setup are ultimately built on it.
Each co_await suspends compute_answer until the corresponding read_value_async() operation completes, without blocking the thread — control returns to the caller while the wait is pending. co_return a + b; ends the coroutine and delivers the sum through the promise type, exactly the way an ordinary function's return statement delivers a value to its caller.
#include <coroutine>
Task<int> compute_answer()
{
int a = co_await read_value_async();
int b = co_await read_value_async();
co_return a + b;
}The Awaiter Interface: await_ready, await_suspend, await_resume
What co_await actually does with its operand depends on three member functions the operand (or a type it converts to) must provide, collectively called the awaiter interface. await_ready() returns bool — true means the value is already available and suspension can be skipped entirely. await_suspend(handle) is called when suspension does happen, given a std::coroutine_handle referring to the now-suspended coroutine, and it decides what happens next (schedule a resumption on another thread, register a callback, or resume immediately by returning false). await_resume() is called when the coroutine resumes, and its return value becomes the result of the whole co_await expression.
await_ready() always returns false here, so the coroutine always suspends. await_suspend() immediately resumes it via handle.resume() — a contrived example, but it shows the shape every real awaitable follows. The standard library provides two ready-made awaitables for the common cases: std::suspend_always (await_ready always false — always suspend) and std::suspend_never (await_ready always true — never actually suspend).
#include <coroutine>
struct LogOnResume
{
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> handle) const noexcept
{
// In real code this might hand `handle` to a scheduler,
// a timer, or an I/O completion callback.
handle.resume();
}
void await_resume() const noexcept {}
};Promise Types and Coroutine Handles
The promise_type nested inside a coroutine's return type is the control center the compiler talks to at every stage of the coroutine's life. get_return_object() builds the object actually returned to the caller when the coroutine is first invoked. initial_suspend() and final_suspend() (the latter must be noexcept) decide whether the coroutine pauses right at the start and right at the end. unhandled_exception() is called if an exception escapes the coroutine body. return_value(v) or return_void() handles co_return, and yield_value(v) handles co_yield.
A std::coroutine_handle<Promise> is a non-owning handle to a suspended coroutine's frame — it is how code outside the coroutine resumes it (handle.resume()), checks whether it has finished (handle.done()), reaches its promise object (handle.promise()), or releases the frame's memory (handle.destroy()). Nothing destroys a coroutine frame automatically unless final_suspend() returns std::suspend_never; if it suspends at the end, the frame stays alive until something calls destroy() on its handle explicitly.
Building a Generator
A generator produces a sequence of values lazily — one value per resumption, computed only when it is actually asked for — which is exactly what co_yield is for. Building one means writing a small class that serves as both the coroutine's return type and an iterable wrapper around its handle.
count_up(1, 5) is called: because it uses co_yield, this creates a coroutine frame and immediately calls promise_type::get_return_object(), producing a Generator<int> — the coroutine body has not started running any of its loop yet, because initial_suspend() returns std::suspend_always.
gen.next() calls handle.resume(), which runs the coroutine body until it hits co_yield i for i == 1: that stores 1 in current_value and suspends again.
gen.value() reads current_value out of the promise object, returning 1.
Each subsequent gen.next() resumes the loop for the next value of i, right where it left off, until i exceeds end.
Once the loop finishes and the coroutine reaches its implicit co_return (calling return_void()), final_suspend() suspends it one last time so the frame stays valid for handle.done() to report true, and gen.next() returns false.
When gen goes out of scope, its destructor calls handle.destroy(), releasing the coroutine frame.
#include <coroutine>
#include <iostream>
#include <utility>
template <typename T>
struct Generator
{
struct promise_type
{
T current_value{};
Generator get_return_object()
{
return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(T value) noexcept
{
current_value = std::move(value);
return {};
}
void return_void() noexcept {}
void unhandled_exception() { std::terminate(); }
};
using handle_type = std::coroutine_handle<promise_type>;
explicit Generator(handle_type h) : handle(h) {}
~Generator() { if (handle) handle.destroy(); }
Generator(const Generator&) = delete;
Generator(Generator&& other) noexcept : handle(std::exchange(other.handle, {})) {}
bool next()
{
handle.resume();
return !handle.done();
}
T value() const { return handle.promise().current_value; }
private:
handle_type handle;
};
Generator<int> count_up(
…Coroutines for Async Programming — a Minimal Task Type
Generators use co_yield to produce a sequence one value at a time. The other major use of coroutines is asynchronous programming with co_await, where a coroutine suspends while waiting on something slow (I/O, a timer, another coroutine) without blocking the thread it started on. Writing a full-featured async framework requires a scheduler and real asynchronous I/O primitives, but the shape of a minimal Task<T> type — the return type used for an async-style coroutine — shows the core mechanism.
Task<int>'s initial_suspend() returns std::suspend_never, so add_async starts running immediately when called, rather than waiting for an explicit resume the way count_up did. In a real async framework, a Task's co_await points at real asynchronous operations instead of computing a value directly, and a scheduler resumes suspended tasks as their underlying operations complete — but the promise-type-and-handle mechanics driving all of it are exactly what is shown here.
What single thing turns an ordinary C++ function into a coroutine?
Using co_return, co_yield, or co_await anywhere in the function's body. There is no coroutine keyword — the compiler detects one of these three keywords and generates a state-machine implementation instead of ordinary straight-line code, and the function's return type must satisfy the coroutine requirements (typically by having a nested promise_type).
What is stored in a coroutine's frame, and why does that make suspension and resumption possible?
The frame holds the promise object, copies of the coroutine's parameters, every local variable whose lifetime crosses a suspension point, and the exact point of execution to resume from. Because all of that lives in the frame rather than in a stack frame that disappears on return, the coroutine can suspend (returning control to the caller) and later resume with every value exactly as it was left.
What do await_ready, await_suspend, and await_resume each control in the awaiter interface used by co_await?
await_ready() returns true if the value is already available, letting the coroutine skip suspension entirely. await_suspend(handle) runs when suspension does happen and decides what happens to the suspended coroutine next (for example, scheduling it to resume later). await_resume() runs when the coroutine resumes, and its return value becomes the result of the co_await expression.
Why does a Generator built with co_yield need to call handle.destroy() in its own destructor rather than relying on the coroutine to clean itself up automatically?
When final_suspend() returns std::suspend_always (as Generator's does), the coroutine frame stays alive after the coroutine body finishes, so that handle.done() can still be queried on it. Nothing destroys that frame automatically in that case — the code holding the handle is responsible for calling destroy() exactly once, which is why Generator wraps the handle in an RAII destructor.
Why is calling resume() or destroy() twice on the same std::coroutine_handle undefined behavior?
std::coroutine_handle is a plain, non-owning handle with no reference counting or ownership tracking built in. Resuming a coroutine that has already run to completion, or destroying a frame that has already been destroyed, accesses memory or state that is no longer valid to touch, which the standard leaves as undefined behavior rather than a defined error.
What does this program print, and at what point does the coroutine body actually start executing its loop?
It prints "1 4 9 16". The coroutine body does not start running any of the loop when squares(4) is called — promise_type::initial_suspend() returns std::suspend_always, so the coroutine is created suspended before its first statement. Execution of the loop only begins on the first call to gen.next(), which resumes the coroutine up through the first co_yield (i == 1, yielding 1), and each subsequent next() call resumes it for the next value of i until i exceeds 4, at which point handle.done() becomes true and the loop in main exits.
Write a C++ coroutine-based generator that lazily yields the first n Fibonacci numbers using co_yield, and a main() that prints them space-separated. Reuse the Generator<T> template shown in the article.
fibonacci keeps a and b as ordinary local variables inside the coroutine frame, so their values survive across every co_yield a; suspension. Each call to gen.next() resumes the loop right where it paused, advances a and b by one Fibonacci step, and the next co_yield hands back the updated a — producing 0 1 1 2 3 5 8 13 21 34 lazily, one value per resumption, rather than computing all ten up front.
Write a small C++ program using a coroutine promise type that demonstrates the difference between std::suspend_always and std::suspend_never in initial_suspend(), by printing a message immediately if the coroutine starts eagerly versus only after the first resume if it starts lazily.
eager_hello uses std::suspend_never for initial_suspend(), so its body runs immediately when called — "eager: running immediately" prints between the two eager print statements in main, before eager_hello() even returns. lazy_hello uses std::suspend_always, so calling it only creates a suspended coroutine frame; nothing inside its body runs until t.handle.resume() is called explicitly, so "lazy: running only after resume" prints only after "after lazy_hello(), before resume", demonstrating that initial_suspend() controls whether a coroutine starts eagerly or waits to be resumed.
#include <coroutine>
#include <iostream>
template <typename T>
struct Task
{
struct promise_type
{
T result{};
Task get_return_object()
{
return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_never initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_value(T value) noexcept { result = std::move(value); }
void unhandled_exception() { std::terminate(); }
};
using handle_type = std::coroutine_handle<promise_type>;
handle_type handle;
explicit Task(handle_type h) : handle(h) {}
~Task() { if (handle) handle.destroy(); }
T get_result()
{
if (!handle.done())
handle.resume();
return handle.promise().result;
}
};
Task<int> add_async(int a, int b)
{
co_return a + b;
}
int main()
{
Task<int> task = add_async(3, 4);
std::cout << task.get_result() << "\n";
return 0;
}#include <coroutine>
#include <iostream>
template <typename T>
struct Generator
{
struct promise_type
{
T current_value{};
Generator get_return_object() { return Generator{std::coroutine_handle<promise_type>::from_promise(*this)}; }
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(T v) noexcept { current_value = v; return {}; }
void return_void() noexcept {}
void unhandled_exception() { std::terminate(); }
};
using handle_type = std::coroutine_handle<promise_type>;
handle_type handle;
explicit Generator(handle_type h) : handle(h) {}
~Generator() { if (handle) handle.destroy(); }
bool next() { handle.resume(); return !handle.done(); }
T value() const { return handle.promise().current_value; }
};
Generator<int> squares(int n)
{
for (int i = 1; i <= n; ++i)
co_yield i * i;
}
int main()
{
Generator<int> gen = squares(4);
while (gen.next())
std::cout << gen.value() << " ";
std::cout << "\n";
}