Painless C++ Coroutine-Part 3

Gajendra Gulgulia
8 min readJun 9, 2021

--

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:

  1. Connecting the coroutine awaiter and the foo
  2. Connecting the coroutine promise_type and the foo via awaiter.

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_resumebut 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:

  1. Revisiting section 6.5 and use the templated version of the awaiter and instantiate/use the awaiter correctly with co_await.
  2. Following this make following changes to the structure of suspend_neverand the coroutine foo.

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:

  1. of accessing the promise object by pointer in the main
  2. changing the prom->val_ ,
  3. 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:

  1. modify suspend_never::await_ready to return false .
  2. use bool returning version of suspend_never::await_suspend and after assigning the promise object to the pointer member variable, return false. (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.

--

--

Gajendra Gulgulia
Gajendra Gulgulia

Written by Gajendra Gulgulia

I'm a backend software developer and modern C++ junkie. I run a modern cpp youtube channel (https://www.youtube.com/@masteringmoderncppfeatures6302/videos)

No responses yet

Write a response