afertig-2023-cppindia-con-cpp-coroutines-from-scratch
afertig-2023-cppindia-con-cpp-coroutines-from-scratch
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++ Coroutines from scratch 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 chat = Fun(); E Creation of the coroutine
13
14 std::cout << chat.listen(); F Trigger the machine
15
16 chat.answer("Where are you?\n"s); G Send data into the coroutine
17
18 std::cout << chat.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 using Handle = std::coroutine_handle <promise_type >; A Shortcut for the handle type
5 Handle mCoroHdl{}; B
6
7 explicit Chat(promise_type* p) : mCoroHdl{Handle::from_promise(*p)} {} C Get the handle form the promise
8 Chat(Chat&& rhs) noexcept : mCoroHdl{std::exchange(rhs.mCoroHdl , nullptr)} {} D Move only!
9
10 ~Chat() noexcept E Care taking, destroying the handle if needed
11 {
12 if(mCoroHdl) { mCoroHdl.destroy(); }
13 }
14
15 std::string listen() F Activate the coroutine and wait for data.
16 {
17 if(not mCoroHdl.done()) { mCoroHdl.resume(); }
18 return std::move(mCoroHdl.promise()._msgOut);
19 }
20
21 void answer(std::string msg) G Send data to the coroutine and activate it.
22 {
23 mCoroHdl.promise()._msgIn = std::move(msg);
24 if(not mCoroHdl.done()) { mCoroHdl.resume(); }
25 }
26 };
Coroutine code
promise_type Transformed user
co_yield "Hello!\n"s; written code
Chat chat = Fun();
T std::cout <<
co_await std::string{};
std::cout << chat.listen();
co_return "Here!\n"s;
chat.answer("Where are you?\n"s);
std::string listen();
std::cout << chat.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 };
Next task:
Plastic surgeon required!
I’m sure we all would like to use a
rangebased forloop instead of
while!
■ Adding support for rangebased for loops et. al. 1 struct sentinel {};
2
■ We need an iterator which fullfils the iteratorconcept: equal 3 struct iterator {
comparable, incrementable, dereferenceable. 4 Handle mCoroHdl{};
5
■ This type is declared inside Generator, but you’re free to 6 bool operator==(sentinel) const
write a more general version. 7 {
8 return mCoroHdl.done();
9 }
10
11 iterator& operator++()
12 {
13 mCoroHdl.resume();
14 return *this;
15 }
16
17 const int operator *() const
18 {
19 return mCoroHdl.promise()._val;
20 }
21 };
1 // struct Generator {
■ Adding support for the iterator to Generator of the
2 // ...
coroutine. 3 iterator begin() { return {mCoroHdl}; }
4 sentinel 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.
■ constexprfunctions cannot be coroutines. Subsequently, this is true for constevalfunctions.
■ 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 returntype or with a concept type cannot be a coroutine. auto with trailing returntype 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 BYND, Version 3.0 http://creativecommons.org/licenses/bynd/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++ Coroutines from scratch, NDC TechTown, September 20
Training Classes
■ Programming with C++20, CppCon, September 27 30
■ Modern C++: When Efficiency Matters, CppCon, October 09 12
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 lecturer for
C++ for standards 11 to 23.
Andreas is involved in the C++ standardization committee, in which the new stan
dards are developed. At international conferences, he presents how code can be
written better. He publishes specialist articles, e.g., for iX magazine, and has pub
lished several textbooks 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 working as a trainer and consultant, 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.