Painless C++ Coroutines-Part 4

Gajendra Gulgulia
4 min readJun 13, 2021

In part one, two and three of the series explained step by step on how to create a coroutine with co_await operato. In the first part of the series, I laid out the foundational principles of C++20 coroutines and demonstrated how a simple coroutine can be created which stays in a suspended state. In the second part of the series, I demonstrated how the coroutine can be resumed through the gradually delved deeper into the underlying principles of coroutine and the order of the implicit calls that are made upon creation, suspension and resuming the coroutine with a lot of examples. Finally in part 3 of this series, I demonstrate how to pass data back and forth between coroutine and its caller and concluded the article with the infinite integer number generator coroutine.

In the fourth part of this series, I want to demonstrate the usage of co_yield operator and how to create a generic generator

9. Coroutine with co_yield

In section 7.3 I demonstrated how to pass a data from a coroutine to its caller. But that technique is quite clumsy and is not really necessary just when returning a data from the coroutine to the caller is required and not the other way round. An astute reader might remember the quote from cpp reference page in section 1 of this series:

2. keyword co_yield to suspend execution returning a value ...

Quoting the co_yield section from cpp reference page:

co_yield expr is equivalent to:
co_await promise.yield_value(expr)

But before we check what this mean, lets check what gcc 10.2 compiler tells us (check here) when an integer is used with co_yield

ReturnObject foo(){co_yield 2;}int main(){auto h =  foo();}Compiler stderr
<source>: In function 'ReturnObject foo()':
<source>:26:5: error: no member named 'yield_value' ...
co_yield 2;

This indicates that ReturnObject::promise_type is missing the yield_value method. The parameter of yield_value must match the type that co_yield is expected to evaluate. The only restriction in the function signature of yield_value method is that it should return an awaiter/awaitable. Therefore the implementation of yield_value in our case looks like

struct ReturnObject {
struct promise_type {
int val_;
...
std::suspend_always yield_value(int value){
val_ = value;
return {};
}
};
...
};

With just this much change, the expression co_yield 2 works and can be accessed in main without the need to do the convoluted magic with a custom awaiter as was demonstrated in section section 7.3 .

using PromType = ReturnObject::promise_type;ReturnObject foo(){
co_yield 2;
co_yield 3;
}
int main(){
auto h = foo();
PromType* prom = &h.promise();
std::cout << "From main: " << prom->val_ <<"\n";
h();
std::cout << "From main: " << prom->val_ <<"\n";
}

The working code for this section is here. Now with the new tool, co_yield under our belt the number of lines of code in our original infinite sequence generator (demonstrated at the end of section 7.3, and here again) is reduced by half and without the need to capture the promise object and manipulate it within the coroutine:

ReturnObject generator(){
for(int i=0; ; i++){
co_yield i;
}
}

check the full working example here

10. A generic generator with co_yield

To create a generic generator that not only works with integer but also with any other type for e.g. float, double, bool or a user defined type, we need to resort to template programming. But there’s very little change that needs to be done and intermediate C++ programmers might find it quite intuitive to fill in the templatized code. But for the sake of completeness, I’ll show the changes anyway.

  1. First templatize the ReturnObject with template parameter T and instead of having promise_type::val_ of type int , change it to a the generic type T .
template<typename T>
struct ReturnObject {
struct promise_type {
T val_;
...
};
...
};

2. In the return signature of the coroutine use the appropriate template parameter. An example coroutine with float number generator looks like

ReturnObject<float> generator(){
for(int i=0; ; i++){
co_yield 1.234 + static_cast<float>(i);
}
}

3. Now the original ReturnObject::promise_type changes to the one with correct template parameter. For the version with float for e.g., it becomes ReturnObject<float>::promise_type.

using PromType =  ReturnObject<float>::promise_type;
int main(){
std::coroutine_handle<PromType> h = generator();
...
}

Complete example for the generic generator can be found here.

Conclusion

In this tutorial I further build upon the generator example that was introduced in part-3 but with co_await operator. In the fifth part of the tutorial series, I’ll cover the usage of co_return operator in a coroutine and then discuss following which I’ll touch upon few points about the std::coroutine_handle<T> member functions done and destroy.

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.

--

--