Coroutines: Suspendable Functions
C++20 coroutines are functions that can suspend execution and resume later, enabling asynchronous programming and lazy generators without callbacks.
Coroutine Keywords
A function is a coroutine if it contains any of:
co_await: Suspend until expression completesco_yield: Suspend and produce a valueco_return: Complete and optionally return a value
generator<int> counter() {
for (int i = 0; i < 5; ++i) {
co_yield i; // Suspend and produce value
}
}
Basic Generator Example
Simple generator coroutine:
#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() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(T value) {
current_value = value;
return {};
}
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> coro;
generator(std::coroutine_handle<promise_type> h) : coro(h) {}
~generator() { if (coro) coro.destroy(); }
bool next() {
coro.resume();
return !coro.done();
}
T value() {
return coro.promise().current_value;
}
};
generator<int> range(int start, int end) {
for (int i = start; i < end; ++i) {
co_yield i;
}
}
// Usage:
auto gen = range(0, 5);
while (gen.next()) {
std::cout << gen.value() << '\\n';
}
Coroutine Mechanics
Promise Type
Every coroutine requires a promise_type that defines behavior:
struct promise_type {
// Called to create the coroutine's return object
ReturnType get_return_object();
// Should coroutine suspend before executing body?
auto initial_suspend(); // Returns suspend_always or suspend_never
// Should coroutine suspend after completing?
auto final_suspend() noexcept;
// Handle co_yield value
auto yield_value(T value);
// Handle co_return
void return_void();
// OR
void return_value(T value);
// Exception handling
void unhandled_exception();
};
Coroutine Handle
Manages coroutine state:
std::coroutine_handle<PromiseType> handle;
handle.resume(); // Resume coroutine execution
handle.done(); // Check if coroutine completed
handle.destroy(); // Destroy coroutine state
handle.promise(); // Access promise object
co_await Expression
Suspend until awaitable completes:
Task async_operation() {
auto result = co_await some_async_call();
// Resumed when async_call completes
co_return result;
}
Awaitable Type
struct awaitable {
bool await_ready(); // Can we skip suspension?
void await_suspend(std::coroutine_handle<>); // Called when suspending
T await_resume(); // Called when resuming, returns value
};
Infinite Generator
generator<int> fibonacci() {
int a = 0, b = 1;
while (true) {
co_yield a;
auto next = a + b;
a = b;
b = next;
}
}
auto fib = fibonacci();
for (int i = 0; i < 10; ++i) {
fib.next();
std::cout << fib.value() << ' ';
}
// 0 1 1 2 3 5 8 13 21 34
Lazy Evaluation
Coroutines enable lazy evaluation—values computed on demand:
generator<int> lazy_transform(const std::vector<int>& vec) {
for (int x : vec) {
co_yield x * x; // Computed only when requested
}
}
Async Task Pattern
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() {}
};
};
Task async_work() {
co_await some_operation();
co_await another_operation();
co_return;
}
Range-Based for with Coroutines
Make generator iterable:
template<typename T>
struct generator {
// ... previous code ...
struct iterator {
std::coroutine_handle<promise_type> coro;
iterator& operator++() {
coro.resume();
return *this;
}
bool operator!=(const iterator&) const {
return !coro.done();
}
T operator*() const {
return coro.promise().current_value;
}
};
iterator begin() {
coro.resume();
return {coro};
}
iterator end() {
return {nullptr};
}
};
// Now works with range-based for:
for (int x : range(0, 5)) {
std::cout << x << ' ';
}
Coroutine State
Coroutine state is heap-allocated (can be optimized away):
// State includes:
// - Parameters
// - Local variables
// - Promise object
// - Suspension point information
Compared to Traditional Approaches
| Approach | Callbacks | State Machines | Coroutines |
|---|---|---|---|
| Readability | Poor | Medium | Excellent |
| Complexity | High | High | Low |
| Error Handling | Difficult | Complex | Natural |
| Performance | Fast | Fast | Fast |
Conceptual Check
What makes a function a coroutine?
Runtime Environment
Interactive Lab
1#include <iostream>
2
3// Simplified generator for demonstration
4struct SimpleGen {
5 int current = 0;
6 int end;
7
8 SimpleGen(int e) : end(e) {}
9
10 bool next() {
11 if (current < end) {
12 ++current;
13 return true;
14 }
15 return false;
16 }
17
18 int value() const { return current - 1; }
19};
20
21// Simulates generator behavior without actual coroutines
22SimpleGen simulate_range(int start, int end) {
23 return SimpleGen(end - start);
24}
25
26int main() {
27 std::cout << "Simulated coroutine generator:\n";
28 auto gen = simulate_range(0, 5);
29
30 while (gen.next()) {
31 std::cout << gen.value() << ' ';
32 }
33 std::cout << '\n';
34
35 return 0;
36}
System Console
Waiting for signal...