🚀 C++ Coroutines — concise tutorial
What are coroutines?
Coroutines are functions that can suspend and later resume execution. They make asynchronous, lazy, or incremental control flow easier to write. C++ exposes coroutines via the keywords co_await, co_yield, and co_return, plus small library types such as std::suspend_always, std::suspend_never, and std::coroutine_handle.
Why use coroutines?
- Write asynchronous code with straightforward control flow.
- Build generators (lazy sequences) without manual state machines.
- Integrate with event loops and async I/O more naturally than raw threads.
Basic usage (short)
co_yield value: suspend and produce a value (useful for generators).co_await expr: await an awaitable (suspends according to the awaitable's behavior).co_return value: finish the coroutine and optionally provide a result.
Coroutines require a coroutine-return type that defines a nested promise_type which the compiler uses to manage the coroutine frame and lifecycle.
Minimal generator example
This is a small single-file example that yields integers. Save as generator.cpp and compile with a recent C++20-capable compiler.
#include <coroutine>
#include <iostream>
struct Generator {
struct promise_type {
int value;
std::suspend_always yield_value(int v) noexcept { value = v; return {}; }
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
Generator get_return_object() noexcept { return Generator{std::coroutine_handle<promise_type>::from_promise(*this)}; }
void return_void() noexcept {}
void unhandled_exception() { std::terminate(); }
};
using handle_t = std::coroutine_handle<promise_type>;
explicit Generator(handle_t h) : h_(h) {}
~Generator() { if (h_) h_.destroy(); }
bool next() { if (!h_) return false; h_.resume(); return !h_.done(); }
int current() const { return h_.promise().value; }
private:
handle_t h_;
};
Generator counter(int n) {
for (int i = 0; i < n; ++i) co_yield i;
}
int main() {
auto g = counter(5);
while (g.next()) std::cout << "value: " << g.current() << '\n';
}
Key notes:
- co_yield maps to promise_type::yield_value which stores the yielded value and suspends.
- The caller resumes the coroutine via the handle; the example exposes next()/current() as a minimal API.
Simple awaiter example (illustrates co_await)
This toy awaiter suspends and immediately resumes the coroutine (demo only — a real scheduler would resume later).
#include <coroutine>
#include <iostream>
struct OneShot {
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) const noexcept { h.resume(); }
void await_resume() const noexcept {}
};
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
};
Task demo() {
std::cout << "before await\n";
co_await OneShot{};
std::cout << "after await\n";
}
int main() { demo(); }
Key notes:
- await_ready() controls immediate continuation vs suspension.
- await_suspend() receives the coroutine handle and may schedule resume later.
- await_resume() returns an awaited value (if any).
How to compile / try
- Save an example as
example.cpp(use one example per file). - Compile with a modern compiler:
# clang
clang++ -std=c++20 example.cpp -O2 -o example
# g++ (recent versions)
g++ -std=c++20 example.cpp -O2 -o example
If you see errors mentioning <coroutine>, try:
- a newer compiler, or
- replacing #include <coroutine> with #include <experimental/coroutine> for older toolchains.
Common pitfalls & tips
- Header differences: older toolchains use
<experimental/coroutine>. - Coroutines do not automatically create threads — resumption is explicit and must be coordinated by your scheduler.
- Take care when resuming coroutine handles across threads — protect shared state.
- Keep coroutine bodies small for easier debugging.
Summary
- Coroutines simplify async and lazy patterns in C++.
- Start with simple generators and awaiters to learn the promise/awaiter model.
- For production, consider established libraries (cppcoro) or integrate coroutines into your event loop.