0% found this document useful (0 votes)
11 views

afertig-2024-meeting-cpp-online-cpp20s-coroutines-for-beginners

Uploaded by

akshat.jain30
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
11 views

afertig-2024-meeting-cpp-online-cpp20s-coroutines-for-beginners

Uploaded by

akshat.jain30
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 22

C++20’s Coroutines for

Beginners
Presentation Material

Meeting C++ online, Online, 2024-04-04

Andreas Fertig Write unique code!


© 2024 Andreas Fertig
AndreasFertig.com
All rights reserved

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.

Planning, typesetting and cover design: Andreas Fertig


Cover art and illustrations: Franziska Panter https://franziskapanter.com
Production and publishing: Andreas Fertig
C++20’s Coroutines for Beginners

Style and conventions


The following shows the execution of a program. I used the Linux way here and skipped supplying the
desired output name, resulting in a.out as the program name.
$ ./a.out
Hello, C++!

• <string> stands for a header file with the name string

• [[xyz]] marks a C++ attribute with the name xyz.

© 2024 Andreas Fertig


https://AndreasFertig.com 3
[email protected]
C++20’s Coroutines for Beginners

fertig
adjective /ˈfɛrtɪç/

finished
ready
complete
completed

Andreas Fertig C++20’s Coroutines for Beginners 2


v1.0

Function vs. Coroutine comparison

Caller Function Caller Coroutine


call call
suspend

co_yield
resume

suspend
co_yield
resume

co_return
return

Andreas Fertig C++20’s Coroutines for Beginners 3


v1.0

© 2024 Andreas Fertig


https://AndreasFertig.com 4
[email protected]
C++20’s Coroutines for Beginners

What are Coroutines?


■ The term coroutine has been well-established in computer science since it was first coined in 1958 by Melvin Conway
[1].
■ They come in two different forms:
■ Stackfull
■ Stackless (which is what we have in C++)

■ 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.

Andreas Fertig C++20’s Coroutines for Beginners 4


v1.0

Interacting with a coroutine


■ Coroutines can be paused and resumed.
■ co_yield or co_await pause a coroutine.
■ co_return ends a coroutine.

Keyword Action State


co_yield Output Suspended
co_return Output Ended
co_await Input Suspended

Andreas Fertig C++20’s Coroutines for Beginners 5


v1.0

© 2024 Andreas Fertig


https://AndreasFertig.com 5
[email protected]
C++20’s Coroutines for Beginners

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.

■ An awaitable type that comes into play once we use co_await.


■ We also often use another part, an iterator.

■ 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.

Andreas Fertig C++20’s Coroutines for Beginners 6


v1.0

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

© 2024 Andreas Fertig


https://AndreasFertig.com 6
[email protected]
C++20’s Coroutines for Beginners

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 }

Andreas Fertig C++20’s Coroutines for Beginners 8


v1.0

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 };

Andreas Fertig C++20’s Coroutines for Beginners 9


v1.0

© 2024 Andreas Fertig


https://AndreasFertig.com 7
[email protected]
C++20’s Coroutines for Beginners

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 };

Andreas Fertig C++20’s Coroutines for Beginners 10


v1.0

Coroutine customization points


User written C++ Compiler implementation

User Code Chat (wrapper) Coroutine

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 …

Allocated by the compiler


and placed on the heap
Optional

Andreas Fertig C++20’s Coroutines for Beginners 11


v1.0

© 2024 Andreas Fertig


https://AndreasFertig.com 8
[email protected]
C++20’s Coroutines for Beginners

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).

Andreas Fertig C++20’s Coroutines for Beginners 12


v1.0

Helper types for Coroutines


■ For yield_value, initial_suspend, final_suspend, as well as co_await / await_transform, we have two helper
types in the Standard Template Library (STL):
■ std::suspend_always: The method await_ready always returns false, indicating that an await expression always suspends as it
waits for its value.
■ std::suspend_never: The method await_ready always returns true, indicating that an await expression never suspends.

1 struct suspend_always { 1 struct suspend_never {


2 constexpr bool await_ready() const noexcept 2 constexpr bool await_ready() const noexcept
3 { 3 {
4 return false ; 4 return true ;
5 } 5 }
6 6
7 constexpr void 7 constexpr void
8 await_suspend(std::coroutine_handle <>) const noexcept 8 await_suspend(std::coroutine_handle <>) const noexcept
9 {} 9 {}
10 10
11 constexpr void await_resume() const noexcept {} 11 constexpr void await_resume() const noexcept {}
12 }; 12 };

Andreas Fertig C++20’s Coroutines for Beginners 13


v1.0

© 2024 Andreas Fertig


https://AndreasFertig.com 9
[email protected]
C++20’s Coroutines for Beginners

Another task for a coroutine:


Interleave two std::vector objects.

Andreas Fertig C++20’s Coroutines for Beginners 14


v1.0

Interleaving two std::vector s

a b

2 4 6 8 3 5 7 9

2 3 4 5 6 7 8 9

Andreas Fertig C++20’s Coroutines for Beginners 15


v1.0

© 2024 Andreas Fertig


https://AndreasFertig.com 10
[email protected]
C++20’s Coroutines for Beginners

Interleaving two std::vector s


■ The interleave coroutine function.
1 Generator interleaved(std::vector <int> a, std::vector <int> b)
2 {
3 auto lamb = [](std::vector <int>& v) −> Generator {
4 for(const auto& e : v) { co_yield e; }
5 };
6
7 auto x = lamb(a);
8 auto y = lamb(b);
9
10 while(not x.finished() or not y.finished()) {
11 if(not x.finished()) {
12 co_yield x.value();
13 x.resume();
14 }
15
16 if(not y.finished()) {
17 co_yield y.value();
18 y.resume();
19 }
20 }
21 }

Andreas Fertig C++20’s Coroutines for Beginners 16


v1.0

Interleaving two std::vector s


■ The promise from the coroutine.

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 };

Andreas Fertig C++20’s Coroutines for Beginners 17


v1.0

© 2024 Andreas Fertig


https://AndreasFertig.com 11
[email protected]
C++20’s Coroutines for Beginners

Interleaving two std::vector s


■ A generator for our coroutine function interleaved.

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 }

Andreas Fertig C++20’s Coroutines for Beginners 18


v1.0

Interleaving two std::vector s


■ How to use interleaved.
1 void Use()
2 {
3 std::vector a{2, 4, 6, 8};
4 std::vector b{3, 5, 7, 9};
5
6 Generator g{interleaved(std::move(a), std::move(b))};
7
8 while(not g.finished()) {
9 std::cout << g.value() << '\n';
10
11 g.resume();
12 }
13 }

Andreas Fertig C++20’s Coroutines for Beginners 19


v1.0

© 2024 Andreas Fertig


https://AndreasFertig.com 12
[email protected]
C++20’s Coroutines for Beginners

Next task:
Plastic surgeon required!
I’m sure we all would like to use a
range-based for-loop instead of
while!

Andreas Fertig C++20’s Coroutines for Beginners 20


v1.0

Interleaving two std::vector s - Beautification

■ 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 };

Andreas Fertig C++20’s Coroutines for Beginners 21


v1.0

© 2024 Andreas Fertig


https://AndreasFertig.com 13
[email protected]
C++20’s Coroutines for Beginners

Interleaving two std::vector s - Beautification

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 // };

1 std::vector a{2, 4, 6, 8};


2 std::vector b{3, 5, 7, 9};
3
4 Generator g{interleaved(std::move(a), std::move(b))};
5
6 for(const auto& e : g) { std::cout << e << '\n'; }

Andreas Fertig C++20’s Coroutines for Beginners 22


v1.0

Another task:
Scheduling multiple tasks.

Andreas Fertig C++20’s Coroutines for Beginners 23


v1.0

© 2024 Andreas Fertig


https://AndreasFertig.com 14
[email protected]
C++20’s Coroutines for Beginners

Cooperative vs. preemptive multitasking


With preemptive multitasking, the thread has no control over:
■ when it runs,
■ on which CPU or,
■ for how long.
In cooperative multitasking, the thread decides:
■ how long it runs, and
■ when it is time to give control to another thread.

■ Instead of using locks as in preemptive multitasking, we say co_yield or co_await.

Andreas Fertig C++20’s Coroutines for Beginners 24


v1.0

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 }

Andreas Fertig C++20’s Coroutines for Beginners 25


v1.0

© 2024 Andreas Fertig


https://AndreasFertig.com 15
[email protected]
C++20’s Coroutines for Beginners

Scheduling multiple tasks


■ Two exemplary tasks.
■ To suspend execution a task must call co_await reaching into the scheduler.

1 Task taskA(Scheduler& sched) 1 Task taskB(Scheduler& sched)


2 { 2 {
3 std::cout << "Hello, from task A\n"sv; 3 std::cout << "Hello, from task B\n"sv;
4 4
5 co_await sched.suspend(); 5 co_await sched.suspend();
6 6
7 std::cout << "a is back doing work\n"sv; 7 std::cout << "b is back doing work\n"sv;
8 8
9 co_await sched.suspend(); 9 co_await sched.suspend();
10 10
11 std::cout << "a is back doing more work\n"sv; 11 std::cout << "b is back doing more work\n"sv;
12 } 12 }

Andreas Fertig C++20’s Coroutines for Beginners 26


v1.0

Scheduling multiple tasks


■ The Scheduler.
1 struct Scheduler {
2 std::list<std::coroutine_handle <>> _tasks{};
3
4 bool schedule()
5 {
6 auto task = _tasks.front();
7 _tasks.pop_front();
8
9 if(not task.done()) { task.resume(); }
10
11 return not _tasks.empty();
12 }
13
14 auto suspend()
15 {
16 struct awaiter : std::suspend_always {
17 Scheduler& _sched;
18
19 explicit awaiter(Scheduler& sched) : _sched{sched} {}
20 void await_suspend(std::coroutine_handle <> coro) const noexcept { _sched._tasks.push_back(coro); }
21 };
22
23 return awaiter{*this};
24 }
25 };

Andreas Fertig C++20’s Coroutines for Beginners 27


v1.0

© 2024 Andreas Fertig


https://AndreasFertig.com 16
[email protected]
C++20’s Coroutines for Beginners

Scheduling multiple tasks

■ The Task type holding the coroutines promise_type.

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 };

Andreas Fertig C++20’s Coroutines for Beginners 28


v1.0

Scheduling multiple tasks - an alternative

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 }

Andreas Fertig C++20’s Coroutines for Beginners 29


v1.0

© 2024 Andreas Fertig


https://AndreasFertig.com 17
[email protected]
C++20’s Coroutines for Beginners

Scheduling multiple tasks - an alternative


■ Two exemplary tasks.
■ To suspend execution a task must say co_await this time calling the operator co_await of an independent type
suspend.

1 Task taskA() 1 Task taskB()


2 { 2 {
3 std::cout << "Hello, from task A\n"sv; 3 std::cout << "Hello, from task B\n"sv;
4 4
5 co_await suspend{}; 5 co_await suspend{};
6 6
7 std::cout << "a is back doing work\n"sv; 7 std::cout << "b is back doing work\n"sv;
8 8
9 co_await suspend{}; 9 co_await suspend{};
10 10
11 std::cout << "a is back doing more work\n"sv; 11 std::cout << "b is back doing more work\n"sv;
12 } 12 }

Andreas Fertig C++20’s Coroutines for Beginners 30


v1.0

Scheduling multiple tasks - an alternative


■ The Scheduler.

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 };

Andreas Fertig C++20’s Coroutines for Beginners 31


v1.0

© 2024 Andreas Fertig


https://AndreasFertig.com 18
[email protected]
C++20’s Coroutines for Beginners

Scheduling multiple tasks - an alternative

■ The operator co_await interacting with the scheduler.

1 static Scheduler gScheduler{};


2
3 struct suspend {
4 auto operator co_await()
5 {
6 struct awaiter : std::suspend_always {
7 void await_suspend(std::coroutine_handle <> coro) const noexcept { gScheduler.suspend(coro); }
8 };
9
10 return awaiter{};
11 }
12 };

Andreas Fertig C++20’s Coroutines for Beginners 32


v1.0

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.

■ Lambdas, on the other hand, can be coroutines.

Andreas Fertig C++20’s Coroutines for Beginners 33


v1.0

© 2024 Andreas Fertig


https://AndreasFertig.com 19
[email protected]
C++20’s Coroutines for Beginners

I am Fertig.

C++20 Coroutine Cheat Sheet


Programming
with C++20
Concepts, Coroutines,
Ranges, and more

Andreas Fertig

fertig.to/subscribe fertig.to/bpwcpp20

Andreas Fertig C++20’s Coroutines for Beginners 34


v1.0

Used Compilers & Typography


Used Compilers
■ Compilers used to compile (most of) the examples.
■ GCC 13.2.0
■ Clang 17.0.0

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/

Andreas Fertig C++20’s Coroutines for Beginners 35


v1.0

© 2024 Andreas Fertig


https://AndreasFertig.com 20
[email protected]
C++20’s Coroutines for Beginners

References
[1] KNUTH D., The Art of Computer Programming: Volume 1: Fundamental Algorithms. Pearson Education, 1997.

Images:
37: Franziska Panter

Andreas Fertig C++20’s Coroutines for Beginners 36


v1.0

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 C++20’s Coroutines for Beginners 37


v1.0

© 2024 Andreas Fertig


https://AndreasFertig.com 21
[email protected]
C++20’s Coroutines for Beginners

About Andreas Fertig

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.

Andreas Fertig C++20’s Coroutines for Beginners 38


v1.0

© 2024 Andreas Fertig


https://AndreasFertig.com 22
[email protected]

You might also like