C++ Coroutines in Practice
Coroutines in practice means running one or more generators and consuming their values in a chosen order. Each generator keeps its own paused place between calls.
A fibonacci generator
The two local values survive every pause. Each resume adds one step of the sequence.
#include <coroutine>
#include <iostream>
class Generator {
public:
struct promise_type {
int current;
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(int value) {
current = value;
return {};
}
void return_void() {
}
void unhandled_exception() {
}
};
explicit Generator(std::coroutine_handle<promise_type> handle) {
h = handle;
}
~Generator() {
h.destroy();
}
bool next() {
h.resume();
return !h.done();
}
int value() {
return h.promise().current;
}
private:
std::coroutine_handle<promise_type> h;
};
Generator fibonacci() {
int a = 0;
int b = 1;
while (true) {
co_yield a;
int nextValue = a + b;
a = b;
b = nextValue;
}
}
int main() {
Generator gen = fibonacci();
for (int k = 0; k < 8; k++) {
gen.next();
std::cout << gen.value() << std::endl;
}
return 0;
}Making and taking take turns
The generator prints each value as it makes it. Main prints after each next() call, so the two sets of lines take turns.
#include <coroutine>
#include <iostream>
class Generator {
public:
struct promise_type {
int current;
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(int value) {
current = value;
return {};
}
void return_void() {
}
void unhandled_exception() {
}
};
explicit Generator(std::coroutine_handle<promise_type> handle) {
h = handle;
}
~Generator() {
h.destroy();
}
bool next() {
h.resume();
return !h.done();
}
int value() {
return h.promise().current;
}
private:
std::coroutine_handle<promise_type> h;
};
Generator maker() {
for (int i = 1; i <= 4; i++) {
std::cout << "Made: " << i * 10 << std::endl;
co_yield i * 10;
}
}
int main() {
Generator gen = maker();
for (int k = 0; k < 4; k++) {
gen.next();
std::cout << "Took: " << gen.value() << std::endl;
}
return 0;
}Two generators alive at once
Each object advances only when its own next() is called. The first column holds squares and the second holds cubes.
#include <coroutine>
#include <iostream>
class Generator {
public:
struct promise_type {
int current;
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(int value) {
current = value;
return {};
}
void return_void() {
}
void unhandled_exception() {
}
};
explicit Generator(std::coroutine_handle<promise_type> handle) {
h = handle;
}
~Generator() {
h.destroy();
}
bool next() {
h.resume();
return !h.done();
}
int value() {
return h.promise().current;
}
private:
std::coroutine_handle<promise_type> h;
};
Generator squares() {
for (int i = 1; i <= 10; i++) {
co_yield i * i;
}
}
Generator cubes() {
for (int i = 1; i <= 10; i++) {
co_yield i * i * i;
}
}
int main() {
Generator sq = squares();
Generator cu = cubes();
for (int k = 0; k < 3; k++) {
sq.next();
cu.next();
std::cout << sq.value() << " " << cu.value() << std::endl;
}
return 0;
}Example
#include <coroutine>
#include <iostream>
class Generator {
public:
struct promise_type {
int current;
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(int value) {
current = value;
return {};
}
void return_void() {
}
void unhandled_exception() {
}
};
explicit Generator(std::coroutine_handle<promise_type> handle) {
h = handle;
}
~Generator() {
h.destroy();
}
bool next() {
h.resume();
return !h.done();
}
int value() {
return h.promise().current;
}
private:
std::coroutine_handle<promise_type> h;
};
Generator multiples() {
for (int i = 3;; i += 3) {
co_yield i;
}
}
int main() {
Generator gen = multiples();
while (gen.next()) {
int v = gen.value();
if (v >= 30) {
break;
}
std::cout << v << std::endl;
}
std::cout << "Stopped early" << std::endl;
return 0;
}Key points
- A generator can run for as long as the caller keeps asking.
- Prints inside the body and prints in main take turns in call order.
- Two generator objects hold two separate paused states.
- Destroying the wrapper ends a sequence early and frees the paused body.
In this example, the multiples generator could run forever. Main stops asking once a value reaches 30. The closing line prints, and destroying the wrapper ends the paused sequence.
Note
The word noexcept on final_suspend promises this step never throws. The promise gets its own lesson later.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
Two generator objects are advanced in turn. What keeps their values from mixing?