Painless C++ Coroutines-Part 1

Incidentally I found the mechanics of coroutines quite obfuscating given the number of implicit and under-the-hood calls to several methods associated with coroutine and their return type objects. Even with tons of articles and posts on the web, I started to have a feeling that coroutines is an arcane feature that can be understood by the very few who are either in committee or are renowned authors of blogs, books and articles or experienced C++ library authors.

But after a month of arduous efforts and experiments, trials and errors to understand coroutine, I decided to write a tutorial series that can be used by any intermediate C++ developer to understand coroutine .

Introduction

C++ 20’s coroutines are a tough nut to crack and cannot be explained in a mere few lines. My post is organized in incremental sections and explains the coroutine mechanics in great details in 12 sections split in 5 parts (i.e. 5 articles in the tutorial series) with tons of examples and code snippets. Each section is written with details that builds up on the details from previous section. Efforts have been made to lay out the idea, that I understood from my experiments and understanding, of why something has to be done the way it has been.

Before starting, I emphasize that this series is not an article that one expects to take away something from by a cursory read. Rather it is a tutorial series that may strike chords with only those who are seriously committed to understanding the coroutines and for them the piece of advice is to try out the example codes and experiment with them. Even the playing with the pseudocodes I provide in the beginning can be helpful in the form of compiler errors.

1. What are coroutines?

I’ll first quote important points from cpp reference page on when a function can be a coroutine

A function is a coroutine if its definition does any of the following:
1. uses the co_await operator to suspend execution ...
2. keyword co_yield to suspend execution returning a value ...
3. keyword co_return to complete execution returning a value ...

Next the restrictions on a coroutine as per the cpp reference page:

Coroutines cannot use variadic arguments, plain return statements, or placeholder return types (auto or Concept). Constexpr functions, constructors, destructors, and the main function cannot be coroutines.

For those who are encountering the coroutines for the first time the important information here is that a function with cannonical return statement we all are used to cannot be a corutine.

2. Simplest coroutine and (not) its return type/return object

2.1 Problem with conventional return type in coroutines

A conventional function returns by returning some result or returning nothing to its caller. In either case we specify the return type corresponding to the result or void when the function returns nothing.

In contrast, as observed in first section, a coroutine cannot have return statement. Even if a coroutine doesn't return any result to its caller, it has to have a some bespoke return object ( which I henceforth call ReturnObject) in its signature which implements certain interface containing a nested promise_type with a certain set of functionalities, details of which will be touched upon in section 2.2.

Before I go on to explain what our ReturnObject should be, I feel it's better to understand with an example, what it should not be!

I have two synthetic non-fuctional example to bolster the idea that a corutine needs a custom return object. One of them uses co_return and another uses co_await. While co_return looks like return, it can be used only in the context of coroutines that return to its caller, in contrast co_await can be used in coroutines that may or may not return to its caller. One more important point to note about co_await is that co_awaitis an operator and expects a valid operand, say expr, that has some characteristic and together the expression looks like co_await expr , which also will be discussed in section 3 to keep the current discussion to the point. With this much in our head, lets look and process the examples:

int foo(){co_return 2;} // (1) compilation error 
int bar(){co_await 2;} //(2) compilation error
int foo2(){return 2;} //(3) ok
int bar2(){return 2} //(4) ok

The compiler error messages for (1) and (2) with gcc 10.2 looks like:

In function 'int foo()':
error: unable to find the promise type for this coroutine
int foo(){co_return 2;}
^~~~~~~~~
In function 'void bar()':
error: unable to find the promise type for this coroutine
int bar(){co_await 2;}
^~~~~~~~

Here the compiler error message also are as obfuscating as the coroutines themselves and doesn’t give much hint as to what exactly may be wrong. The promise type that the compiler complains in both the cases is actually the promise_type object which is associated with the return type of the coroutine. In both the cases the error is related to the return type object which needs to implement the interface with the correct nested promise_type object.

(Just for the sake of completeness, the error with co_yield in a function with incorrect return objct will be exactly the same.)

2.2 ReturnObject of a coroutine

The return type in the signature of corutine should contain a class/struct object, which I call ReturnObject, and within this object, another nested class/struct called promise_type (with exactly the same spelling). It is needed because upon a coroutine instantiation (which will be discussed later) the compiler will generate code which will call the functions within the nested promise_type object. With above ideas and counter-examples the first pieces of a ReturnObject can be put together.

A valid ReturnObject for any coroutine’s reutrn signature must be a class/struct with nested type promise_type with the following member functions.

  1. valid constructor for the promise_type. In simple cases, a default constructor will suffice.
  2. get_return_object method: return type of this method is same as the outer scoped ReturnObject .
  3. initial_suspend method: return type of this method is an Awaitable object.
  4. final_suspend method: also return type of this method is an Awaitable object.
  5. void unhandled_exception() method: to handle exception if it occurs.

(More on Awaitable objects in a later part)

struct ReturnObject{    struct promise_type{        promise_type() = default; //(1)        ReturnObject get_return_object() {return {}; } //(2)        Awaitable initial_suspend() {return {};}   //(3)default ctor        Awaitable final_suspend()   {return {};}  //(4)default ctor        void unhandled_exception(){}              //(5)    }; //end of struct promise_type}; //end of struct ReturnObject

This is the simplest return object with no customization in the ReturnOject. To create a coroutine with the above return object such as:

ReturnObject bar(){co_await expr;}

The result of co_await expr is used to obtain first the Awaitable and finally the Awaiter object by series of calls that are generated by the compiler. But for now we need to understand what Awaitable and Awaiter objects are from just the surface and use a concrete object instead of expr to be able to create a simple coroutine.

3. Awaitable object and co_await operator

Fortunately, the valid expr that can be used with a co_await, in its simplest form, can also be an Awaitable object and vice versa. C++ standard offers two (simple) trivial awaitables:

  1. std::suspend_always : tells co_await to suspend the coroutine
  2. std::suspend_never : tells co_await to not suspend the corutine.

By using one of the two awaitables instead of the abstract Awaitable within the ReturnObject::promise_type and an as operand to co_await for the pseudo code in section 2.2 , a simple compilable coroutine can be created:

struct ReturnObject{
struct promise_type{
promise_type() = default;
ReturnObject get_return_object() {return {}; }
std::suspend_always initial_suspend() {return {};} //Note(1)
std::suspend_never final_suspend(){return {};} //Note(2)
void unhandled_exception(){}
};
};
ReturnObject foo(){
std::cout << "1. hello from coroutine";
co_await std::suspend_always{}; //Note(3) std::cout << "2. hello from coroutine";
}
//call foo
int main(){foo(); }

In the modified code snippet above, the changes have been commented with a Note marker. co_await std::suspend_always{}; indicates that the coroutine should stop execution as soon as this line of code is evaluated. Thus common sense should hint that the first print statement within foo should be printed in the standard output.

But, one can observe that the call to foo doesn't print any of the statement within the coroutine, even though the co_await within foo is used after the first print statement and the coroutine seems to be suspended before it could start executing. Clearly something else suspended the coroutine even before it could start executing its body.

4. Instantiating and suspending the coroutine.

Since we’re talking about coroutines which can be suspended and executed again and the mechanics of coroutines call are more complex than a normal function call, contrary to cpp reference page, I like to think of coroutine being instantiated before it starts its first execution. This helps to create a picture that when the coroutine is first used, some under-the-hood calls happens, just like a constructor is called in the background when a custom object is instantiated.

This could be observed in the previous call to foo where the call got suspended even before the first co_await expression is reached. At this point, I would like to highlight three important calls that happens under the hood when the coroutine is executed the first time (or in my words: instantiated) from the cpp reference page here:

When a coroutine begins execution, it performs the following:
1. calls the constructor for the promise object . . .
2. calls promise.get_return_object() and keeps the result in a local variable . . .
3. calls promise.initial_suspend() and co_awaits its result . . .

Lets look at call to foo in the light of the three statements above once again. As soon as foo is called in the main for the first time the compiler generated code calls (in order ) the (1) default constructor,(2) get_return_object and (3) initial_suspend within the nested promise_type and the result of the call to initial_suspend is co-awaited by it. As the name suggests, initial_suspend tells the compiler whether or not the coroutine should be kept suspended when executed for the first time or in my words when the coroutine is instantiated.

If we look at the ReturnObject in section 3, initial_suspend method returns an awaitable std::suspend_always which is co-awiated by the behind the scene compiler call and as a result the coroutine gets suspended even before it could reach the first print statement within the foo method . If instead std::suspend_never is returned by initial_suspend method then one can observe that the first print statement within the coroutine gets executed.

struct ReturnObject{    struct promise_type{
...
std::suspend_never initial_suspend() {return {};}
...
}; //end of struct promise_type};//end of struct ReturnObject

The code snippet above only shows the diff code within the ReturnObject . A functional code using the above changes can be found on this link.

This idea can be easily extrapolated and it can be observed that replacing std::suspend_always with std::suspend_never within the foo causes the second line within the foo to get printed and the coroutine behaves like a normal function that returns nothing to its caller.

There’s another example that I came up with, for readers to play with and to be able to understand the sequence of calls to the functions and the awaiter objects that are explicitly and implicitly co-awaited with the help of print statements.

I encourage the readers to get their hands dirty and try using multiple co_await expressions within foo coroutine, exchanging std::suspend_never and std::suspend_always wherever possible and test how the sequence of implicit calls are affected using the last example.

Conclusion

There’s a lot to be covered in C++ coroutines and we’ve only scratched the surface. I promise that the series will remain easy to understand with each topic section building on top of the previous ones and there will be no leaps in the explanations as I myself encountered in scores of posts about coroutines. As promised sections 5,6 and 7 are covered in the post Painless C++ Coroutines -Part 2.

Support me by becoming a member

If you like my tutorials and articles, please consider supporting me by becoming a member through my medium referral link and get unlimited access to all articles on medium.com for just $5 a month.

--

--

--

I'm a backend software developer and from time to time I also like to explore web development

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Why you should build a starter project to enjoy your side-projects

Detections of Past, Present, and Future

Apache proxy pass ignore ssl

Why Do We Need RabbitMQ?

What is the Application of Programming in Data Science

Writing Clean Code : Better way to Coding

It is a very hard to time be a physician right now, especially one who is working in the ICU

python string manipulation important questions

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Gajendra Gulgulia

Gajendra Gulgulia

I'm a backend software developer and from time to time I also like to explore web development

More from Medium

C++20 Concurrency: part-3 request_stop and stop_token for std::jthread

C++ Templates: What is std::enable_if and how to use it?

Friendly Introduction Pointers in C++

Modern C++ in Advent of Code: Day19