C++ coroutine — 4
Table of contents
Introduction
- Coroutines are a generalization of functions in C++
- They are designed to make writing asynchronous code much easier
Parts
promise_type
coroutine_handle
- awaiter
- passed to
co_wait
std::suspend_always
std::suspend_never
- passed to
Awaiter methods
bool await_ready(){}
- Whether the co_await expression suspends.
- if false is returned, then
await_suspend
is called, to (mostly) suspend
void await_suspend(){}
- May suspend the coroutine, or schedule the coroutine state for destruction
void await_resume(){}
- May return the result of the entire
co_await
expression
- May return the result of the entire
It is possible to create your own awaiters, things that can act as operands to the co_await operator, by setting up structs/classes that overload the co_await operator
struct My_Awaitable {
auto operator co_await() {
return std::suspend_always{};
}
};
Function
- Can be called
- Can return something
Coroutines
- Can be called
- Can return something
- Can be paused
- Can be resumed
When paused the function state is save on heap topically.
- local variables
- parameters
- pointer to resume this function
Lazy computation
auto f = some_coroutine(); // store computation info in f
// Use f to manipulate the computation
Keywords
C++ 20 introduces three keywords that help pause and resume coroutines
co_yield
: suspends the execution and returns a valueco_return
: completes execution and optionally returns a valueco_await
: suspends the execution until resumed
If a function has one of those keywords, it becomes a coroutine. There is no other special syntax for coroutines.
Which function can be a coroutine
It’s not every function in C++ that can be a coroutine. The functions below can’t be coroutines.
constexpr
functions- constructors
- destructors
- the main function
C++20 warnings
C++ 20 doesn’t provide actual usable coroutine types like Corolype
It provides the low level infrastructure to build them (promises, awaitables, coroutine handles,…
Building your own coroutine types is not recommended. It’s only reserved for hard core, highly experienced library developers who really know what they’re doing
It is expected that C++23 will provide high level coroutine types built into C++, ready to use just by including some headers
If you want to use them know, there are third party libraries that can help, like cppcoro and some others
See Also
```cpp
#include <cassert>
#include <coroutine>
#include <iostream>
template <typename T>
struct generator {
struct promise_type {
T m_value;
generator get_return_object() { return generator(this); }
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() noexcept {
std::rethrow_exception(std::current_exception());
}
std::suspend_always yield_value(T val) {
m_value = val;
return {};
}
void return_void() { std::cout << "Returning void..." << std::endl; }
};
generator(promise_type* p)
: m_handle(std::coroutine_handle<promise_type>::from_promise(*p)) {}
~generator() {
std::cout << "Handle destroyed..." << std::endl;
m_handle.destroy();
}
T operator()() {
assert(m_handle != nullptr);
m_handle.resume();
return (m_handle.promise().m_value);
}
std::coroutine_handle<promise_type> m_handle;
};
generator<int> generate_numbers() {
std::cout << "generate_numbers starting" << std::endl;
co_yield 10; // Return 10 and pause
std::cout << "After stop point #1" << std::endl;
co_yield 20;
std::cout << "After stop point #2" << std::endl;
co_yield 30;
std::cout << "After stop point #3" << std::endl;
std::cout << "generate_numbers ending" << std::endl;
}
generator<int> infinite_number_stream(int start = 0) {
auto value = start;
for ([[maybe_unused]] int i = 0;; ++i) {
std::cout << "In infinite_number stream..." << std::endl;
co_yield value;
++value;
}
}
generator<int> range(int first, int last) {
while (first != last) {
co_yield first++;
}
}
int main() {
auto task1 = generate_numbers();
std::cout << "value : " << task1() << std::endl;
std::cout << "value : " << task1() << std::endl;
std::cout << std::boolalpha;
std::cout << "value : " << task1() << std::endl;
std::cout << "coro done : " << task1.m_handle.done() << std::endl;
task1();
std::cout << "coro done : " << task1.m_handle.done() << std::endl;
auto task2 = infinite_number_stream();
for (size_t i{}; i < 10; ++i) {
std::cout << "infinite value : " << task2() << std::endl;
}
// Range
auto task3 = range(0, 25);
for (size_t i{}; i < 26; ++i) {
std::cout << "range value[" << i << "] : " << task3() << std::endl;
}
std::cout << "Done!" << std::endl;
return 0;
}
Possible output
value : generate_numbers starting
10
value : After stop point #1
20
value : After stop point #2
30
coro done : false
After stop point #3
generate_numbers ending
Returning void...
coro done : true
infinite value : In infinite_number stream...
0
infinite value : In infinite_number stream...
1
infinite value : In infinite_number stream...
2
infinite value : In infinite_number stream...
3
infinite value : In infinite_number stream...
4
infinite value : In infinite_number stream...
5
infinite value : In infinite_number stream...
6
infinite value : In infinite_number stream...
7
infinite value : In infinite_number stream...
8
infinite value : In infinite_number stream...
9
range value[0] : 0
range value[1] : 1
range value[2] : 2
range value[3] : 3
range value[4] : 4
range value[5] : 5
range value[6] : 6
range value[7] : 7
range value[8] : 8
range value[9] : 9
range value[10] : 10
range value[11] : 11
range value[12] : 12
range value[13] : 13
range value[14] : 14
range value[15] : 15
range value[16] : 16
range value[17] : 17
range value[18] : 18
range value[19] : 19
range value[20] : 20
range value[21] : 21
range value[22] : 22
range value[23] : 23
range value[24] : 24
range value[25] : Returning void...
24
Done!
Handle destroyed...
Handle destroyed...
Handle destroyed...