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_return
s 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.