A slight digression
Before continuing on the next post which will be about co_await
, awaitable
/awaiter
interfaces and scheduling of coroutines across threads, I would like to implement a small variant of the previous blog entry.
In this entry I make a small dig into decoupling the future object (coroutine return object) from the promise_type
.
Well, at least decoupling as far as referencing the promise_type
directly in the future class.
Looking up the promise_type
using coroutine_traits
In the previous post I specified the promise type inside the future class as:
template<typename T> struct Gen{ using promise_type=val_promise_type<Gen>; ... };
What if I don't want to explicitly name the promise_type
inside the future?
Can I do that?
Yup, that is doable within the framework of C++20 coroutines.
A C++ traits
class is simply a lookup table even though a rather convoluted one, where types are looked up using types as keys.
A std::coroutine_traits
class pointpoints a promise_type
given the name of a future
(coroutine return object) together with the parameter types to the coroutine.
In our example, the promise type is Gen<T>
and the coroutine has the following signature where T
is of type int
:
Gen<int>genint(int n);
To lookup the corresponding future in the std::coroutine_traits
the compilation machinery will use the following specialization we write for std::coroutine_trait
:
// traits for looking up the promise object from a Gen<T> type template <typename T, typename... Args> struct std::coroutine_traits<Gen<T>,Args...>{ class promise_type{ ... }; };
The specialization is wider than needed since it allows for any number of coroutine parameters of unspecified types. In our case, we could have written the trait simply as:
template <> struct std::coroutine_traits<Gen<int>,int>{ class promise_type{ ... }; };
or as this if we want to force one parameter to the coroutine but leave the parameter type unspecified:
template <typename T> struct std::coroutine_traits<Gen<T>,T>{ class promise_type{ ... }; };
From here on, I will stay with the last std::coroutine_traits
specification.
The complete code
To summerize I'll show the complete code here. There are some noticeable differences from the code in the previous blog post that I will point out.
The first difference is that the promise_type
is not explicitly mentioned in the future (Gen<T>
class).
Instead, the compilation machinery finds the promise_type
by looking it up inside the std::coroutine_traits
class that we defined.
A consequence of that we don't know the promise_type
inside the future is that we cannot call a method on the promise_type<void>
to retrieve the yield value.
That is, we cannot retrieve the promise_type
object by calling promise()
on the coroutine handle.
Instead, in the promise_type
where the future is created, we pass as a second parameter to the future constructor a reference to the value stored inside the promise_type
.
Now, we can access the value stored inside the promise_type
object.
This may not look too elegant, but it does solve our problem of not knowing the structure of the promise_type
from inside the future when we need to get hold of the value
.
The C++ documentation states that coroutine_handle<void>
erases the promise_type
.
It should be noted that the coroutine_handle<void>
is still a perfectly good handle for controlling the coroutine.,
With the handle we can resume
the coroutine, or we can check if the coroutine completed (using done()
) without knowing anything about the promise_type
.
#include <iostream> #include <vector> #include <optional> #include <coroutine> using namespace std; // future that does not know the promise_type // (promise_type is looked up by the compiler through the std::coroutine_traits<Gen<T>,T>) template<typename T> class Gen{ public: // ctor, dtor Gen(coroutine_handle<void>handle,T const&valref):handle_(handle),valref_(valref){} ~Gen(){handle_.destroy();} // get current value // (we have a reference to the last yield value in the promise object)( T value()const{return valref_;} // get next item optional<T>next(){ if(handle_.done())return std::nullopt; handle_(); // resume coroutine: handle_.resume <--> handle_() return handle_.done()?optional<T>{}:value(); } private: coroutine_handle<void>handle_; // keep handle so we can control coroutine (we don't know the promise_type - so void) T const&valref_; // ref to value we store inside promise }; // traits for looking up the promise object from a Gen<T> type template <typename T> struct std::coroutine_traits<Gen<T>,T>{ // promise used when we need to store a single yield value class promise_type{ public: Gen<T>get_return_object(){return Gen<T>(coroutine_handle<promise_type>::from_promise(*this),val_);} void return_void(){} std::suspend_always yield_value(T val){val_=val;return {};} std::suspend_always initial_suspend(){return {};} std::suspend_always final_suspend(){return {};} void unhandled_exception(){std::terminate();} private: T val_; }; }; // simple integer sequence Gen<int>genint(int n){ for(int i=0;i<n;++i)co_yield i; } // test program int main(){ auto gen=genint(5); while(auto val=gen.next())cout<<val.value()<<endl; }
A quick conceptual snapshot
It's time to take birds eye snapshot view of what we have done so for. It's difficult to not get lost in the technicalities of the C++20 coroutine machinations.
The four main constructs we have dealt with are the promise_type
, the future
object, the coroutine
itself and the coroutine_handle
.
The relationships between them are roughly (in our example):
Before closing, it's worth mentioning that we could also have stored the value
in the future
object.
Each time a new yield value was produced, we could have propagate it to the future
so that a user could get hold of it.
Of course, we would then have had to store a reference to the future
inside the promise_type
object.