C++20 introduced coroutines as a language‑level feature that allows developers to write asynchronous code in a sequential style. A coroutine is a function that can suspend execution (co_await, co_yield, or co_return) and resume later, making it ideal for lazy evaluation, pipelines, and non‑blocking I/O. Below we explore the core concepts, a simple implementation, and best practices.
1. Coroutine Basics
| Term | Meaning |
|---|---|
co_await |
Suspends until the awaited expression completes. |
co_yield |
Produces a value to the caller and suspends. |
co_return |
Returns a final value and ends the coroutine. |
promise_type |
Provides the machinery that the compiler uses to manage coroutine state. |
awaitable |
Any type that can be awaited; it must provide await_ready(), await_suspend(), and await_resume(). |
When you write a coroutine, the compiler generates a state machine that manages these suspensions. The return type of the coroutine is typically a specialization of std::future, std::generator, or a user‑defined type that hides the promise.
2. A Simple generator Example
Below is a minimal implementation of a generator that yields Fibonacci numbers. This demonstrates how to create an awaitable type and how to use co_yield.
#include <coroutine>
#include <iostream>
#include <optional>
template <typename T>
class generator {
public:
struct promise_type;
using handle_type = std::coroutine_handle <promise_type>;
generator(handle_type h) : handle(h) {}
~generator() { if (handle) handle.destroy(); }
// Iterator interface
struct iterator {
handle_type h;
iterator(handle_type h_) : h(h_) { advance(); }
iterator& operator++() { advance(); return *this; }
const T& operator*() const { return *h.promise().current_value; }
bool operator==(std::default_sentinel_t) const { return !h || h.done(); }
private:
void advance() { if (!h.done()) h.resume(); }
};
iterator begin() { return iterator(handle); }
std::default_sentinel_t end() { return {}; }
private:
handle_type handle;
};
template <typename T>
struct generator <T>::promise_type {
std::optional <T> current_value;
generator get_return_object() { return generator{handle_type::from_promise(*this)}; }
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(T value) {
current_value = std::move(value);
return {};
}
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
generator <uint64_t> fibonacci(unsigned n) {
uint64_t a = 0, b = 1;
for (unsigned i = 0; i < n; ++i) {
co_yield a;
auto tmp = a;
a = b;
b = tmp + b;
}
}
Usage:
int main() {
for (auto v : fibonacci(10)) {
std::cout << v << ' ';
}
// Output: 0 1 1 2 3 5 8 13 21 34
}
3. co_await and awaitable
co_await works with types that satisfy the Awaitable concept. The compiler transforms the co_await expression into calls to:
await_ready() // true → skip suspension
await_suspend() // receives the coroutine handle; returns true if suspension needed
await_resume() // value returned after resumption
A trivial awaitable that completes immediately:
struct immediate_awaitable {
bool await_ready() noexcept { return true; }
void await_suspend(std::coroutine_handle<>) noexcept {}
void await_resume() noexcept {}
};
async void example() {
co_await immediate_awaitable{}; // no suspension
}
For I/O, you typically wrap platform APIs (e.g., Boost.Asio, libuv) into awaitables that resume when data arrives.
4. Common Pitfalls
| Issue | Fix |
|---|---|
| Exception safety | Ensure promise_type::unhandled_exception handles or rethrows. |
| Lifetime of awaitables | Avoid returning references to local objects from await_resume. |
| State machine size | Keep coroutine functions small; large state machines hurt stack size. |
| Blocking inside coroutines | Never call blocking APIs directly; wrap them in awaitables that resume asynchronously. |
5. Where to Use Coroutines
- Lazy sequences:
generatortypes that produce values on demand. - Async I/O: Wrapping sockets, files, or database queries.
- Reactive pipelines: Compose streams of data with
co_yield. - Coroutines as continuations: Offload work to a thread pool with custom awaitables.
6. TL;DR
C++ coroutines provide a powerful abstraction for asynchronous and lazy programming. By understanding the promise type, awaitable interface, and coroutine control flow, you can write cleaner, non‑blocking code that integrates seamlessly with existing C++ ecosystems. Start small—create a generator, then evolve to complex I/O workflows—and you’ll unlock the full potential of modern C++.