return, a coroutine uses
co_return returning its result. In this post, I want to implement a simple coroutine using co_return.
You may wonder: Although I have presented the theory behind coroutines, I want to write once more about coroutines. My answer is straightforward and based on my experience. C++20 does not provide concrete coroutines. Instead, C++20 provides a framework for implementing coroutines. This framework consists of more than 20 functions, some of which you must implement and others can override. Based on these functions, the compiler generates two workflows, which define the behavior of the coroutine. To make it short. Coroutines in C++20 are double-edged swords. On one side, they give you enormous power; on the other side, they are pretty challenging to understand. I dedicated over 80 pages to coroutines in my book “C++20: Get the Details“, which has not yet explained everything.
From my experience, using simple coroutines and modifying them is the easiest – maybe only – way to understand them. And this is precisely the approach I’m pursuing in the following posts. I present simple coroutines and modify them. To make the workflow obvious, I put many comments inside and added only so much theory necessary to understand the internals of coroutines. My explanations are not complete and should only serve as a starting point to deepen your knowledge about coroutines.
A Short Reminder
While you can only call a function and return from it, you can call a coroutine, suspend and resume it, and destroy a suspended coroutine.
With the new keywords
co_yield, C++20 extends the execution of C++ functions with two new concepts.
co_await expression it is possible to suspend and resume the execution of the expression. If you use
co_await expression in a function
func, the call
auto getResult = func() does not block if the function call’s result is unavailable. Instead of resource-consuming blocking, you have resource-friendly waiting.
co_yield expression supports generator functions. The generator function returns a new value each time you call it. A generator function is a data stream from which you can pick values. The data stream can be infinite. Therefore, we are at the center of lazy evaluation with C++.
Additionally, a coroutine does not
return its result, a coroutine does
co_return its result.
In this straightforward example
createFuture is the coroutine because it uses one of the three new keywords
co_return, co_yield, or
co_await and it returns a coroutine
MyFuture<int>. What? This is what often puzzled me. The name coroutine is used for two entities. Let me introduce two new terms.
createFuture is a coroutine factory that returns a coroutine object
fut, that can be used to ask for the result:
This theory should be enough. Let’s talk about
Admittedly, the coroutine in the following program
eagerFuture.cpp is the simplest coroutine, I can imagine that still does something meaningful: it automatically stores the result of its invocation.
MyFuture behaves as a future, which runs immediately (see “Asynchronous Function Calls“). The call of the coroutine
createFuture (line 1) returns the future, and the call
fut.get (line 2) picks up the result of the associated promise.
There is one subtle difference to a future: the return value of the coroutine
createFuture is available after its invocation. Due to the lifetime issues of the coroutine, the coroutine is managed by a
std::shared_ptr (lines 3 and 4). The coroutine always uses
std::suspend_never (lines 5 and 6) and, therefore, neither does suspend before it runs nor after. This means the coroutine is immediately executed when the function
createFuture is invoked. The member function
get_return_object (line 7) returns the handle to the coroutine and stores it in a local variable.
return_value (lines 8) stores the result of the coroutine, which was provided by
co_return 2021 (line 9). The client invokes
fut.get (line 2) and uses the future as a handle to the promise. The member function
get finally returns the result to the client (line 10).
You may think it is not worth implementing a coroutine that behaves just like a function. You are right! However, this simple coroutine is an ideal starting point for writing various implementations of futures.
At this point, I should add a bit of theory.
The Promise Workflow
When you use
co_return in a function, the function becomes a coroutine, and the compiler transforms its function body into something equivalent to the following lines.
Do these function names sound familiar to you? Right! These are the member functions of the inner class
promise_type. Here are the steps the compiler performs when it creates the coroutine object as the return value of the coroutine factory
createFuture. It first creates the promise object (line 1), invokes its
initial_suspend member function (line 2), executes the body of the coroutine factory (line 3), and finally, calls the member function
final_suspend (line 4). Both member functions
final_suspend in the program
eagerFuture.cpp return the predefined awaitables
std::suspend_never. As its name suggests, this awaitable suspends never; hence, the coroutine object suspends never and behaves such as a usual function. An awaitable is something you can await on. The operator co_await needs an awaitable. I will write a post about the awaitable and the second awaiter workflow.
From this simplified promise workflow, you can deduce which member functions the promise (
promise_type) at least needs:
- A default constructor
Admittedly, this was not the full explanation but at least enough to get the first intuition about the workflow of coroutines.
You may already guess it. In my next post, I will use this simple coroutine as a starting point for further experiments. First, I add comments to the program to make its workflow explicit, second, I make the coroutine lazy and resume it on another thread.
Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Matthieu Bolt, Stephen Kelley, Kyle Dean, Tusar Palauri, Dmitry Farberov, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, and Bhavith C Achar.
Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.
|My special thanks to Embarcadero|
|My special thanks to PVS-Studio|
|My special thanks to Tipi.build|
|My special thanks to Take Up Code|
I’m happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.
- Embedded Programmierung mit modernem C++ 12.12.2023 – 14.12.2023 (Präsenzschulung, Termingarantie)
Standard Seminars (English/German)
Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.
- C++ – The Core Language
- C++ – The Standard Library
- C++ – Compact
- C++11 and C++14
- Concurrency with Modern C++
- Design Pattern and Architectural Pattern with C++
- Embedded Programming with Modern C++
- Generic Programming (Templates) with C++
- Clean Code with Modern C++
- Phone: +49 7472 917441
- Mobil:: +49 176 5506 5086
- Mail: schulung@ModernesCpp.de
- German Seminar Page: www.ModernesCpp.de
- Mentoring Page: www.ModernesCpp.org
Modernes C++ Mentoring,