Painless C++ Coroutines-Part 5

In the fourth part of the tutorial series, the usage of new operator co_yield in a coroutine was demonstrated and in the end a generic infinite sequence generator was created with it. In this tutorial, I’ll introduce the usage of co_return operator in a coroutine.

11. Coroutine with co_return

Without much a precursor, I’ll quote from section 1 the only mention of co_return operator in the entire tutorial series.

3. keyword co_return to complete execution returning a value ...

Thus co_return keyword signals the end of a coroutine and returns a value. Just like a void returning function can have a return statement, by extension a coroutine that need not return anything can have co_return statement without an operand. So, as you might have already guessed, we’ll look at both types of coroutines

ReturnObject foo1(){co_return;     } //co_return w/o an operand
ReturnObject foo2(){co_return value;} co_return w/i an operand

one by one in the next two subsections. Here I’d like to point out that the ReturnObject::promise_type I’m using is the same as I’ve been using since section 7.2, i.e., the one with a member variable int val_ . Also a piece of advice would be to check out the fourth tutorial on co_yield after which all the steps to implement coroutine with co_return will become intuitive.

11.1 co_return without an operand

If you’ve followed my first and fourth tutorial, you might have already noticed that best approach to understand how write the correct interface of ReturnObject is by simply evoking a compiler error with a trivial code. You can check the compiler error here.

ReturnObject foo(){co_return; } //co_return w/o an operandCompiler stderr:
In function 'ReturnObject foo()':
error: no member named 'return_void'

Since we’re not aiming to return anything with co_return and the compiler complains of a missing member named return_void (and after having been through similar steps to write coroutine with co_yield), it is easy to guess (trust me that’s how I did it!) that ReturnObject::promise_type needs to have an void returning member function called return_void that takes no parameter in its argument list. So lets go ahead and add this member function

struct ReturnObject {
struct promise_type {
...
void return_void(){}
};
...
};

Easy! Check the working code here. Note that the member promise_type::val_ can still be accessed by the caller.

11.2 co_return with an operand

Repeating the same steps as in 11.1, and checking the compiler error on the code which co_returns an integer , for e.g.:

ReturnObject foo(){co_return 121; } //co_return with an operandCompiler stderr:
<source>: In function 'ReturnObject foo()':
<source>: error: no member named 'return_value'

it can be observed that a different member function, in this case return_value, is needed in the interface of ReturnObject::promise_type for the case a value is wished to be co_return-ed. Again by analogy to the steps followed for co_yield , the method return_value has an void (yes that’s correct!) in the return signature and takes in a parameter of type that is desired to be co_return-ed, in this case an int. This change to the interface of ReturnObject::promise_type is below:

struct ReturnObject {
struct promise_type {
...
void return_value(int val){val_ = val;}
};
...
};

The working example can be checked here.

12. End of coroutine execution: implicit co_return and final_suspend

Even if a coroutine doesn’t have a co_return statement, one can verify that the return_void method is executed one last time by adding a print statement in a couroutine with co_await.

struct ReturnObject {
struct promise_type {
...
void return_void(){std::cout << "return_void\n";}
};
...
};
ReturnObject foo(){co_await std::suspend_never{};}
int main(){ foo(); }
stdout
called return_void

Check out the code here. One can observe that the end of coroutine is marked by co_return implicitly. What one needs to consider in the interface of promise_type that it must contain the method promise_type::final_suspend which returns (like promise_type::initial_suspend) an awaiter object which helps in deciding that even after the coroutine has ended execution should the coroutine be finally suspended one last time or not.

So in reality, the implicit function call return_void that could be observed above is not the actual end of coroutine. Call to return_void is followed by a call to final_suspend. If for e.g. the method final_suspend returns std::suspend_always, then the corutine is suspended one final time and the state is saved. This allows programmers to access the promise_type for one last time (via h.promise() for example) and do whatever the programmer intends to do with it before everything is destroyed.

In this case the programmer must free the coroutine handle manually by calling the std::coroutine_handle<T>::destroy() method explicitly. To be sure that coroutine execution has completed, one can call the member function bool std::coroutine_handle<T>::done() before destroying the coroutine handle.

int main(){
std::coroutine_handle<PromType> h = foo();
PromType* prom = &h.promise();
/*
call h.resume() many times
*/
if(h.done()){
/*
do something with prom one last time
*/
h.destroy();
}
}

Here’s a slightly more verbose example of the code with implicit call to return_void followed by a final suspension of the coroutine.

Conclusion

In this part of the series, the usage of the remaining co_return operator has been demonstrated which marks the end of uncovering coroutine with all three operators. So far we’ve dealt with details of programming a coroutine with the operators co_await, co_yield, co_return operators and neglecting the pitfalls one may run into while using them. With Section 12, I’ve made a subtle effort to transition to details of std::coroutine_handle<T>‘s member functions. In the next issue, I’ll try to dive deeper into the usage aspect of coroutines and the pitfalls that are to be avoided while using coroutines.

--

--

--

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

Supremacy - Fight for Glory.

IF you’re a BEGINNER.. here’s how to take what you already know to a next level.

Generics — Java 11

If you want to understand more how the digital world is, learn how to code

Snowflake Sample example

Slow Query Log in AWS MYSQLRDS

More on special requests

Turn your Computer into a Gardener to grow Flowers and Trees with a Turtle and L-Systems (Part 1)

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 1: synchronized output stream

Modern C++ in Advent of Code: Day13

Thanks, Mario, but the code needs fixing — checking TheXTech

Friendly Introduction Pointers in C++