C++20 introduced coroutines, a powerful language feature that lets you write asynchronous code in a style that closely resembles synchronous, sequential code. Unlike traditional callback-based or promise-based approaches, coroutines maintain their state across suspension points, allowing developers to build complex asynchronous workflows with cleaner, more maintainable code.
1. What Are Coroutines?
At its core, a coroutine is a function that can pause its execution (co_await, co_yield, or co_return) and resume later, preserving local variables and the call stack. The compiler transforms the coroutine into a state machine behind the scenes, handling all the bookkeeping for you.
co_await expression; // Suspend until expression is ready
co_yield value; // Return a value and suspend
co_return value; // End the coroutine, returning a final value
2. The Anatomy of a Coroutine
A coroutine has three main parts:
- Promise Type – Defines the interface between the coroutine and the caller. It provides hooks like
get_return_object(),initial_suspend(), andfinal_suspend(). - State Machine – Generated by the compiler; it keeps track of the coroutine’s state and the values of its local variables.
- Suspension Points – Where execution can pause, typically marked by
co_await,co_yield, orco_return.
When you call a coroutine, the compiler generates a coroutine handle (std::coroutine_handle<>)) that the caller can use to resume or inspect the coroutine.
3. A Simple Example
Below is a minimal coroutine that asynchronously reads integers from a stream and sums them:
#include <coroutine>
#include <iostream>
#include <optional>
#include <vector>
struct async_int_stream {
struct promise_type {
std::optional <int> value;
std::coroutine_handle <promise_type> get_return_object() { return std::noop_coroutine(); }
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
std::suspend_always yield_value(int v) {
value = v;
return {};
}
};
std::coroutine_handle <promise_type> h;
explicit async_int_stream(std::coroutine_handle <promise_type> h_) : h(h_) {}
~async_int_stream() { if (h) h.destroy(); }
// Fetch the next value, if available
std::optional <int> next() {
if (!h.done()) h.resume();
return h.promise().value;
}
};
async_int_stream read_integers() {
for (int i = 0; i < 10; ++i) {
co_yield i; // Yield each integer
}
}
int main() {
auto stream = read_integers();
std::optional <int> val;
int sum = 0;
while ((val = stream.next())) {
sum += *val;
std::cout << "Received: " << *val << "\n";
}
std::cout << "Total sum: " << sum << "\n";
}
This program demonstrates how co_yield allows the coroutine to return a value and pause, enabling the caller to consume values one at a time.
4. Practical Use Cases
- Asynchronous I/O – Coroutines can be used to write non-blocking network or file I/O without the overhead of callbacks.
- Lazy Evaluation – Generate large data streams on demand, saving memory and processing time.
- Concurrency Control – Coroutines can be combined with
std::asyncor thread pools to parallelize workloads while keeping code readable.
5. Coroutine Libraries and Frameworks
While the standard library provides the raw building blocks, many libraries abstract these concepts further:
- cppcoro – A lightweight, header-only library providing `generator `, `task`, and other coroutine types.
- Boost.Coroutine2 – Offers stackful coroutines and integration with Boost.Asio.
- Asio – Uses coroutines to simplify asynchronous networking code.
6. Common Pitfalls
- Lifetime Management – Coroutines capture local variables by reference unless moved; ensure they outlive the coroutine if needed.
- Stackful vs. Stackless – Standard coroutines are stackless; stackful coroutines (like those in Boost) have separate stacks and can cause memory issues if misused.
- Exception Safety – Unhandled exceptions inside coroutines propagate to the caller; always handle them or provide
unhandled_exception()in the promise type.
7. Future Directions
C++23 is set to refine coroutine support further, adding features like co_await std::any_of and improved synchronization primitives. Expect tighter integration with other asynchronous paradigms, making coroutines an even more integral part of modern C++.
Coroutines open up a new paradigm for writing clean, efficient asynchronous code. By understanding the underlying state machine, promise type, and suspension points, developers can harness the full power of C++20’s coroutine feature and write code that is both expressive and performant.