What you'll learn
A coroutine is a function that can suspend and resume while preserving its state. The language supplies transformation hooks, not a complete async runtime. You will inspect promise types, awaiters, handles, generators, tasks, cancellation, and the lifetime rules that make production coroutine code safe.
By the end, you'll be able to:
- Explain coroutine state, suspension, and the three coroutine keywords
- Describe promise, awaiter, and coroutine_handle responsibilities
- Build and consume a minimal pull-style generator
- Choose coroutines only with an explicit scheduler, lifetime, cancellation, and error model
Suspension and coroutine state
A function containing co_await, co_yield, or co_returncan be transformed into a coroutine. Its parameters, locals that survive suspension, promise object, and bookkeeping live in a coroutine frame. Calling it usually returns a wrapper that controls or observes that frame.
| Keyword | Purpose |
|---|---|
co_await expression | Possibly suspend until an awaitable is ready |
co_yield value | Publish a value and suspend, typically in a generator |
co_return value | Complete the coroutine through its promise |
coroutine_handle | Non-owning handle used to resume, query, or destroy a frame |
Key idea
Promise and awaiter protocol
The return type identifies a nested promise_type. The compiler asks that promise for the returned wrapper, initial and final suspension policy, yielded or returned values, and exception handling. An awaiter supplies await_ready, await_suspend, and await_resume.
co_await expression
1. Convert expression to an awaiter.
2. await_ready(): continue immediately if true.
3. await_suspend(handle): publish/schedule the continuation.
4. Suspend when the protocol says to.
5. A scheduler or event resumes the coroutine.
6. await_resume(): produce the co_await result or throw.Watch out
await_suspend is lifetime-sensitive. Resuming or destroying a frame twice, or resuming after destruction, is undefined behavior.A minimal generator
A pull generator suspends at each co_yield. The wrapper owns the frame, resumes it to request another value, and destroys it at the end. Production generators need polished iterators and range integration; this focused version exposes the essential mechanics.
#include <coroutine>
#include <exception>
#include <iostream>
#include <utility>
template <typename T>
class Generator {
public:
struct promise_type {
T current{};
std::exception_ptr error;
Generator get_return_object() {
return Generator{handle_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 = std::move(value);
return {};
}
void return_void() noexcept {}
void unhandled_exception() noexcept { error = std::current_exception(); }
};
using handle_type = std::coroutine_handle<promise_type>;
explicit Generator(handle_type handle) : handle_{handle} {}
Generator(const Generator&) = delete;
Generator(Generator&& other) noexcept
: handle_{std::exchange(other.handle_, {})} {}
~Generator() { if (handle_) handle_.destroy(); }
bool next() {
handle_.resume();
if (handle_.done() && handle_.promise().error)
std::rethrow_exception(handle_.promise().error);
return !handle_.done();
}
const T& value() const { return handle_.promise().current; }
private:
handle_type handle_;
};
Generator<int> count_to(int end) {
for (int value{1}; value <= end; ++value) co_yield value;
}
int main() {
auto values{count_to(3)};
while (values.next()) std::cout << values.value() << ' ';
}Tasks and schedulers
A task-like coroutine represents one eventual completion rather than a sequence. Its awaiter typically records the awaiting continuation and resumes it when the task finishes. An event loop, I/O completion system, or thread pool decides where and when execution resumes; the C++ standard library does not yet provide one universal production task type.
- Define whether a task starts eagerly or only when awaited
- State which executor or thread resumes each continuation
- Propagate values and exceptions through the promise
- Make frame ownership and destruction unambiguous
- Avoid silently blocking inside an interface advertised as asynchronous
Note
co_await syntax.Lifetime, cancellation, and errors
Local variables stored in the frame survive suspension, but referenced external objects do not gain lifetime. Cancellation must reach pending awaiters and prevent abandoned work from later resuming a destroyed frame. Exceptions are commonly captured byunhandled_exception and rethrown when a consumer awaits or advances the result.
| Risk | Design response |
|---|---|
| Borrowed object dies while suspended | Own needed state or use a verified shared lifetime |
| Operation is cancelled | Unregister callbacks and complete with a documented cancellation result |
| Awaited operation fails | Store exception or typed error and surface it at await_resume |
| Task wrapper is destroyed | Define whether it cancels, detaches, waits, or destroys safely |
| Continuation resumes on another thread | Document synchronization and executor affinity |
Coroutines versus other tools
Coroutines excel at sequential-looking workflows with many suspension points and at lazy generators. Threads suit blocking independent work; futures suit one-shot results; callbacks integrate with event APIs but can fragment control flow; ranges suit synchronous lazy data transformations. Choose from the actual execution and ownership model.
| Tool | Strong fit |
|---|---|
| Coroutine | Suspendable workflow or generator with an established runtime |
| jthread | Independent blocking or CPU work with scoped lifetime |
| future | One eventual result with simple waiting |
| Callback | Existing event-source integration and small continuations |
| Range view | Synchronous lazy transformation without suspension |
Recap & quick check
Key takeaways
- Coroutine state lives in a frame that survives suspension until an owner destroys it.
- Promise types control results and suspension; awaiters control readiness, suspension, and resumption.
- A generator owns its frame and resumes it to pull each yielded value.
- C++ coroutine syntax does not provide a scheduler, event loop, or universal task abstraction.
- Safe async design must define ownership, cancellation, error propagation, and resumption context.
Quick check
1. Does a coroutine automatically create a new thread?
2. Where do locals that survive suspension normally live?
3. Which awaiter method produces the co_await result?
4. What must production coroutine design define beyond syntax?
Next: Module 25 — Binary Data, Interoperability & Low-Level C++, where C++ meets bytes, native APIs, and platform boundaries.