Painless C++ Coroutine-Part 3

In first part of the issue, Painless C++ Coroutine Part-1, the we laid out all the components that are needed to instantiate a compilable coroutine.
In the second part of the issue, Painless C++ Coroutine Part-2, the demonstrations continued to explain the technique to resume a suspended coroutine by understanding the coroutine handle and its relation with the promise type object. Following this, the huge but important detour was taken to understand awaiter and awaitable objects by creating the custom awaiters.
I continue the journey to finally reveal how to return the data to the caller of the coroutine in section 7 and finally a lazy generator using coroutine in section 8.
7. Returning data from coroutines to caller
This is the exciting part where all the coroutine customization can be used in one place, but it has to be broken down to assimilatable and incremental chunks to manage the complexity. These incremental chunks are divided in three parts:
- Connecting the coroutine
awaiter
and thefoo
- Connecting the coroutine
promise_type
and thefoo
viaawaiter
.
and finally
3. Connecting foo
to main
.
7.1. Connecting the coroutine awaiter
and the foo
At the conclusion of section 6.1, it was mentioned that the return type of expression co_await expr
is same as that of awaiter::await_resume
which till this point has been void
. Fortunately standard doesn't place any restriction on what the return type of await_resume
should be and one can return anything from it and catch the returned data as a result of the evaluation of co_await expr
expression and the returned value can be used within the coroutine. The example below (note the usage of a custom suspend_never
awaiter object):
struct suspend_never{
...
double await_resume() const noexcept {return 10.234;}
};ReturnObject foo(){
double val = co_await suspend_never{};
std::cout << val << "\n";
}int main()
{
std::coroutine_handle<> h = foo();
}//stdout: 10.234
The working code can be found here.
One can try to persist the value val
returned by co_await suspend_never{}
expression over multiple suspend-resume of coroutine by returning pointer or reference from suspend_never::await_resume
but since the operand suspend_never{}
to co_await
is a temporary, every time the coroutine is resumed, the suspend_never::val_
is destroyed along with the temporary object.
struct suspend_never{
double* val_{new double(10.234)}
...
double* await_resume() const noexcept {return val_;}
};ReturnObject foo(){ //temporary suspend_never is destroyed after evaluation
double val1 = co_await suspend_never{};
std::cout << val1 << "\n"; //(1) val1 = 20.234; double val2 = co_await suspend_never{};
std::cout << val2 << "\n"; //(2) == (1)}int main()
{std::coroutine_handle<> h = foo();}//stdout:
10.234
10.234
As can be seen from the stdout, the value of val1
and val2
variable is the same as val_
which is initialized within the awaiter. The link to the functional code is here.
The aim of the demonstration above is to inject the idea that we need some sort of mechanism to persist data across coroutine calls which eventually can be passed to the caller of the coroutine. More objectivley, we need an object whose lifetime is tied with that of the coroutine and not with the awaiter and that it can be returned to the caller of the coroutine. There are two options here: (1) a variable in the coroutine itself ; (2) The promise_type
object associated with the coroutine handle. Former cannot be passed to the caller directly and hence we resort to the latter option by connecting the awaiter to the promise_type
.
7.2. Connecting the promise_type
to awaiter
to foo
Yes there is no grammatical or syntatical error in the above line.. The promise_type
has to be connected to foo
via the awaiter. This looks like a spaghetti, but this is how it has to be. This is a two step process:
- Revisiting section 6.5 and use the templated version of the awaiter and instantiate/use the awaiter correctly with
co_await
. - Following this make following changes to the structure of
suspend_never
and the coroutinefoo
.
2.a. Add a member variable which is a pointer to the promise type which matches the template parameter name
2b. Assign the above pointer member variable the promise type
in await_suspend
by call to the method promise
associated with the coroutine handle.
2.c. Return a pointer to the promise type from await_resume
.
2.d. Within foo
assign the return of co_awiat suspend_always
to another variable.
Lets take a look at the changes:
template<typename PromiseType = void> //1
struct suspend_always{
PromiseType* promise_; //2.a void await_suspend(std::coroutine_handle<PromiseType> h)noexcept
{
promise_ = &h.promise(); //2.b
} PromiseType* await_resume() const noexcept
{
return promise_; //2.c
}
};//alias to make code look less scary
using PromType = ReturnObject::promise_type;ReturnObject foo()
{
// here auto == PromType*
auto promise = co_await suspend_always<PromType>{};//1,2d
}
To test if this really works, a member variable, say int val_
, can be introduced in the definition of promise_type
and it can be printed within the coroutine.
struct ReturnObject {
struct promise_type{
int val_{11};
...
};
...
};ReturnObject foo()
{
...
std::cout << promise_->val;
}
The functional code can be referred here .
7.3. Connecting the foo
too its caller: returning data
We’re finally at a point where the coroutine can be used to pass data back and forth between the caller and itself. In 7.2.2.b, the public method promise
associated with a coroutine handle, which itself is associated with the promise_type
was introduced. In principle, this method can be used to access the underlying promise_type
associated with the coroutine any where and not just within the awaiter (in our case suspend_always
) object. For e.g.
int main(){ std::coroutine_handle<PromType> h = foo(); PromType prom = h.promise();
std::cout << prom.val_;
}
See the working example here.
7.4. Sending data from caller to foo
for processing in suspended state
What if a use case where passing data to the coroutine is required which can then be used while the coroutine is in suspended state? This can also now be easily achieved since the promise variable (1)prom
is reference to the promise object ;and (2) is a pointer member within the awaiter suspend_never
and/or suspend_never
associated with the same coroutine handle h
which is tied to the coroutine execution within the main
.
It is simply a matter:
- of accessing the promise object by pointer in the
main
- changing the
prom->val_
, - resuming the coroutine.
ReturnObject foo(){ //suspending coroutine for first time
co_await std::suspend_always{}; (A) //suspending coroutine for second time
auto promise = co_await suspend_never<PromType>{}; (B) std::cout << ctr ++ << ". Coro finished\n";
}int main(){ //coro suspended due to (A)
std::coroutine_handle<PromType> h = foo(); PromType* prom = &h.promise(); (1)
prom->val_ = 21; (2) //upon resuming, second co_await expr (B) is executed
h(); (3)
}
In the above code snippet I carefully chose the trivial awaitable std::suspend_always
to prevent calling any custom code to be executed during coroutine suspended state for the first time and only after I changed the prom->val_
in main
I evaluate the custom suspend_never<PromType>
awaitable where the val_
is used for further synchronous processing. A slightly more verbose version of the above example is here.
8. A lazy generator with coroutine
Okay cool. Lets put the wealth of coroutine knowledge to practice and create a generator coroutine that generates integer and returns it to caller. This generator coroutine should be, in theory, able to generate a non-repeating values from infinite sequence.
Note that to create such a generator, using a custom awaiter that can return the promise type associated with the coroutine handle only once is sufficient. For further suspension, the trivial awaiter provided by the standard is sufficient.
ReturnObject generator(){
//use custom awaiter only once
auto promise = co_await suspend_always<PromType>{};
for(int i=0;;i++){
promise->val_ = i;
co_await std::suspend_always{}; //use trivial awaiter
}
}int main(){
std::coroutine_handle<PromType> h = generator();
PromType& prom = h.promise();
for(int i=0; i<5; ++i){
std::cout << "From main: " << prom.val_ <<"\n";
h();
}
}stdout:
From main: 0
From main: 0
From main: 1
From main: 2
From main: 3
The coroutine worked but not as expected. The for loop within the main
resumed the coroutine 5 times (from 0 to 4) but the values that was returned from generator is not 0 till 4, but from 0 till 3 and with the value zero appearing twice. Therefore the generator is not generating a non-repeating sequence, the one we aimed for. The code is here.
Of course one can do a minor fix to this by initializing the loop counter in the generator with 1 instead of zero
ReturnObject generator(){
...
for(int i=1; ;i++){
...
}
but such fixes are not easy to generalize, and cannot offer a long term solution. A closer look at the code reveals that the coroutine is suspended by the first evaluation of co_await suspend_always<PromType>{}
, but what is desired is that it should be suspended within the for loop. Hence one can think to simply replace suspend_always<PromType>{}
with suspend_never<PromType>{}
and start the suspension of coroutine by the trivial awaiter object in the for loop.
ReturnObject generator(){
auto promise = co_await suspend_never<PromType>{};
std::cout << promise->val_;
}int main(){
std::coroutine_handle<PromType> h = generator();
}
But there’s a problem with this modification (code here). As discussed at the end of section 6.2, if await_ready
returns true
as in the case of suspend_never
, it implies that the coroutine is ready to execute and should not be suspended and consequently the method await_suspend
is not executed to avoid the overhead of copying coroutine state to the heap. Skipping the call to await_suspend
means that the promise object associated with the coroutine handle is not assigned to the member variable which was initialized as nullptr
.
...
bool await_ready() const noexcept { return true;} //this method is not executed due to above line
void await_suspend(std::coroutine_handle<PromiseType> h)noexcept
{
promise_ = &h.promise();
} //this method returns nullpr
PromiseType* await_resume() const noexcept {return promise_;}
};
and as a result everything falls apart … or not! The way to ameliorate this is to hijack suspend_never
and force it to call await_suspend
method so that the promise_
member variable is assigned the promise type correctly. This can be done by in two steps:
- modify
suspend_never::await_ready
to returnfalse
. - use
bool
returning version ofsuspend_never::await_suspend
and after assigning the promise object to the pointer member variable, returnfalse
. (see section 6.3 for details).
The changes look like below:
bool await_ready() const noexcept { return false;} bool await_suspend(std::coroutine_handle<PromiseType> h)noexcept
{
promise_ = &h.promise();
return false;
}
The working code with above modifications is here.
With just these changes our infinite lazy generator works flawlessly . One can now easily craft a custom generator for e.g one that generates only even or only odd numbers in the sequence. Checkout the generators in action here.
Conclusion
With the third issue, we come to an end of long journey of demistifying and understanding the mechanics of coroutines with co_await
operator . The fourth issue, Painless C++ Coroutine-Part 4, will explain co_yield
which will be much shorter as core concepts that have been covered till now and most of the underlying mechanisms are already explained in this and the previous two issues.
C++20 videos on youtube and course
Subscribe to my youtube channel to learn more about coroutines and other advanced topics on C++20 or browse my course page to get access to all C++20 videos, quizzes and personal support.
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.