afertig-2024-meeting-cpp-online-cpp20s-coroutines-for-beginners
afertig-2024-meeting-cpp-online-cpp20s-coroutines-for-beginners
Beginners
Presentation Material
All programs, procedures and electronic circuits contained in this book have been created to the best of our knowledge and belief and
have been tested with care. Nevertheless, errors cannot be completely ruled out. For this reason, the program material contained in
this book is not associated with any obligation or guarantee of any kind. The author therefore assumes no responsibility and will not
accept any liability, consequential or otherwise, arising in any way from the use of this program material or parts thereof.
Version: v1.0
The work including all its parts is protected by copyright. Any use beyond the limits of copyright law requires the prior consent of the
author. This applies in particular to duplication, processing, translation and storage and processing in electronic systems.
The reproduction of common names, trade names, product designations, etc. in this work does not justify the assumption that such
names are to be regarded as free in the sense of trademark and brand protection legislation and can therefore be used by anyone,
even without special identification.
fertig
adjective /ˈfɛrtɪç/
finished
ready
complete
completed
co_yield
resume
suspend
co_yield
resume
co_return
return
■ Stackless means that the data of a coroutine, the coroutine frame, is stored on the heap.
■ We are talking about cooperative multitasking when using coroutines.
■ Coroutines can simplify your code!
■ We can replace some function pointers (callbacks) with coroutines.
■ Parsers are much more readable with coroutines.
■ A lot of state maintenance code is no longer required as the coroutine does the bookkeeping.
Elements of a Coroutine
■ In C++, a coroutine consists of:
■ A wrapper type. This is the return type of the coroutine function’s prototype.
■ With this type can control the coroutine from the outside. For example, resuming the coroutine or getting data into or from the coroutine by storing a handle to the coroutine in the
wrapper type.
■ The compiler looks for a type with the exact name promise_type inside the return type of the coroutine (the wrapper type). This is
the control from the inside.
■ This type can be a type alias, or
■ a typedef,
■ or you can declare the type directly inside the coroutine wrapper type.
■ A coroutine in C++ is an finite state machine (FSM) that can be controlled and customized by the promise_type.
■ The actual coroutine function which uses co_yield, co_await, or co_return for communication with the world out-
side.
Disclaimer
Please note, I tried to keep the code you will see as simple as
possible. Focusing on coroutines. In production code, I work
more with public and private as well as potential getters and
setters. Additionally, I use way more generic code in production
code to keep repetitions low.
My goal is to help you understand coroutines. I’m confident that
you can improve the code you will see with the usual C++ best
practices.
I also never declare more than one variable per line... slide code is
the only exception.
Andreas Fertig C++20’s Coroutines for Beginners 7
v1.0
Coroutine chat
1 Chat Fun() A Wrapper type Chat containing the promise type
2 {
3 co_yield "Hello!\n"s; B Calls promise_type.yield_value
4
5 std::cout << co_await std::string{}; C Calls promise_type.await_transform
6
7 co_return "Here!\n"s; D Calls promise_type.return_value
8 }
9
10 void Use()
11 {
12 Chat marco = Fun(); E Creation of the coroutine
13
14 std::cout << marco.listen(); F Trigger the machine
15
16 marco.answer("Where are you?\n"s); G Send data into the coroutine
17
18 std::cout << marco.listen(); H Wait for more data from the coroutine
19 }
Coroutine chat
1 struct promise_type {
2 std::string _msgOut{}, _msgIn{}; A Storing a value from or for the coroutine
3
4 void unhandled_exception() noexcept {} B What to do in case of an exception
5 Chat get_return_object() { return Chat{*this}; } C Coroutine creation
6 std::suspend_always initial_suspend() noexcept { return {}; } D Startup
7 std::suspend_always yield_value(std::string msg) noexcept F Value from co_yield
8 {
9 _msgOut = std::move(msg);
10 return {};
11 }
12
13 auto await_transform(std::string) noexcept G Value from co_await
14 {
15 struct awaiter { H Customized version instead of using suspend_always or suspend_never
16 promise_type& pt;
17 constexpr bool await_ready() const noexcept { return true; }
18 std::string await_resume() const noexcept { return std::move(pt._msgIn); }
19 void await_suspend(std::coroutine_handle <>) const noexcept {}
20 };
21
22 return awaiter{*this};
23 }
24
25 void return_value(std::string msg) noexcept { _msgOut = std::move(msg); } I Value from co_return
26 std::suspend_always final_suspend() noexcept { return {}; } E Ending
27 };
Coroutine chat
1 struct Chat {
2 #include "promise −type.h" // Don't do that at work!
3
4 std::coroutine_handle <promise_type > mHandle{}; A
5
6 explicit Chat(promise_type& p)
7 : mHandle{std::coroutine_handle <promise_type >::from_promise(p)} {} B Get the handle form the promise
8
9 Chat(Chat&& rhs) noexcept : mHandle{std::exchange(rhs.mHandle , nullptr)} {} C Move only!
10
11 ~Chat() noexcept D Care taking, destroying the handle if needed
12 {
13 if(mHandle) { mHandle.destroy(); }
14 }
15
16 std::string listen() E Activate the coroutine and wait for data.
17 {
18 if(not mHandle.done()) { mHandle.resume(); }
19 return std::move(mHandle.promise()._msgOut);
20 }
21
22 void answer(std::string msg) F Send data to the coroutine and activate it.
23 {
24 mHandle.promise()._msgIn = std::move(msg);
25 if(not mHandle.done()) { mHandle.resume(); }
26 }
27 };
Coroutine code
promise_type Transformed user
co_yield "Hello!\n"s; written code
Chat macro = Fun();
T std::cout <<
co_await std::string{};
std::cout << marco.listen();
co_return "Here!\n"s;
macro.answer("Where are you?\n"s);
std::string listen();
std::cout << marco.listen(); void answer(std::string);
Coroutine frame
std::coroutine_handle …
A few definitions
■ Task: A coroutine that does a job without returning a value.
■ Generator: A coroutine that does a job and returns a value (either by co_return or co_yield).
a b
2 4 6 8 3 5 7 9
2 3 4 5 6 7 8 9
1 struct promise_type {
2 int _val{};
3
4 Generator get_return_object() { return Generator{*this}; }
5 std::suspend_never initial_suspend() noexcept { return {}; }
6 std::suspend_always final_suspend() noexcept { return {}; }
7 std::suspend_always yield_value(int v)
8 {
9 _val = v;
10 return {};
11 }
12
13 void return_void() noexcept {}
14 void unhandled_exception() noexcept {}
15 };
1 // struct Generator {
2 std::coroutine_handle <promise_type > mHandle{};
3
4 explicit Generator(promise_type& p) noexcept : mHandle{std::coroutine_handle <promise_type >::from_promise(p)} {}
5
6 Generator(Generator&& rhs) noexcept : mHandle{std::exchange(rhs.mHandle , nullptr)} {}
7
8 ~Generator() noexcept
9 {
10 if(mHandle) { mHandle.destroy(); }
11 }
12
13 int value() const { return mHandle.promise()._val; }
14
15 bool finished() const { return mHandle.done(); }
16
17 void resume()
18 {
19 if(not finished()) { mHandle.resume(); }
20 }
Next task:
Plastic surgeon required!
I’m sure we all would like to use a
range-based for-loop instead of
while!
■ Adding support for range-based for loops et. al. 1 struct iterator {
2 std::coroutine_handle <promise_type > mHandle{};
■ We need an iterator which fullfils the iterator-concept: equal 3
comparable, incrementable, dereferenceable. 4 bool operator==(std::default_sentinel_t) const
5 {
■ This type is declared inside Generator, but you’re free to 6 return mHandle.done();
write a more general version. 7 }
8
9 iterator& operator++()
10 {
11 mHandle.resume();
12 return *this;
13 }
14
15 const int operator *() const
16 {
17 return mHandle.promise()._val;
18 }
19 };
1 // struct Generator {
■ Adding support for the iterator to Generator of the
2 // ...
coroutine. 3 iterator begin() { return {mHandle}; }
4 std::default_sentinel_t end() { return {}; }
5 // };
Another task:
Scheduling multiple tasks.
1 void Use()
■ Starting and scheduling two tasks.
2 {
3 Scheduler scheduler{};
4
5 taskA(scheduler);
6 taskB(scheduler);
7
8 while(scheduler.schedule()) {}
9 }
1 struct Task {
2 struct promise_type {
3 Task get_return_object() noexcept { return {}; }
4 std::suspend_never initial_suspend() noexcept { return {}; }
5 std::suspend_never final_suspend() noexcept { return {}; }
6 void return_void() noexcept {}
7 void unhandled_exception() noexcept {}
8 };
9 };
1 void Use()
■ Starting and scheduling two tasks. This time using a
2 {
global object. 3 taskA();
4 taskB();
5
6 while(gScheduler.schedule()) {}
7 }
1 struct Scheduler {
2 std::list<std::coroutine_handle <>> _tasks{};
3
4 void suspend(std::coroutine_handle <> coro) { _tasks.push_back(coro); }
5
6 bool schedule()
7 {
8 auto task = _tasks.front();
9 _tasks.pop_front();
10
11 if(not task.done()) { task.resume(); }
12
13 return not _tasks.empty();
14 }
15 };
Coroutine restrictions
■ There are some limitations in which functions can be a coroutine and what they must look like.
■ constexpr-functions cannot be coroutines. Subsequently, this is true for consteval-functions.
■ Neither a constructor nor a destructor can be a coroutine.
■ A function using varargs. A variadic function template works.
■ A function with plain auto as a return-type or with a concept type cannot be a coroutine. auto with trailing return-type works.
■ Further, a coroutine cannot use plain return. It must be either co_return or co_yield.
■ And last but not least, main cannot be a coroutine.
I am Fertig.
Andreas Fertig
fertig.to/subscribe fertig.to/bpwcpp20
Typography
■ Main font:
■ Camingo Dos Pro by Jan Fromm (https://janfromm.de/)
■ Code font:
■ CamingoCode by Jan Fromm licensed under Creative Commons CC BY-ND, Version 3.0 http://creativecommons.org/licenses/by-nd/3.0/
References
[1] KNUTH D., The Art of Computer Programming: Volume 1: Fundamental Algorithms. Pearson Education, 1997.
Images:
37: Franziska Panter
Upcoming Events
Talks
■ C++20 Coroutinen - Ein Einstieg, ADC, May 07
Training Classes
■ C++20 Coroutinen - Ein Einstieg, ADC, May 06
For my upcoming talks you can check https://andreasfertig.com/talks/.
For my courses you can check https://andreasfertig.com/courses/.
Like to always be informed? Subscribe to my newsletter: https://andreasfertig.com/newsletter/.
Andreas Fertig, CEO of Unique Code GmbH, is an experienced trainer and consultant
for C++ for standards 11 to 23.
Andreas is involved in the C++ standardization committee, developing the new stan-
dards. At international conferences, he presents how code can be written better. He
publishes specialist articles, e.g., for iX magazine, and has published several text-
books on C++.
With C++ Insights (https://cppinsights.io), Andreas has created an internationally rec-
ognized tool that enables users to look behind the scenes of C++ and thus under-
Photo: Kristijan Matic www.kristijanmatic.de
stand constructs even better.
Before training and consulting, he worked for Philips Medizin Systeme GmbH for ten
years as a C++ software developer and architect focusing on embedded systems.
You can find Andreas online at andreasfertig.com.