Last time, we diagrammed out how the pieces of a coroutine fit together. Today we’ll fill in the diagram with code.
Fortunately, most of the hard work has already been done for us by the result_
class we already wrote. We just need to adapt it to the format required by the coroutine specification.
namespace std::experimental { template <typename T, typename... Args> struct coroutine_traits<result_holder<T>, Args...> { struct promise_type { result_holder<T> holder; result_holder<T> get_return_object() const noexcept { return holder; } void return_value(T const& v) const { holder.set_result(v); } void unhandled_exception() const noexcept { holder.set_exception(std::current_exception()); } suspend_never initial_suspend() const noexcept { return{}; } suspend_never final_suspend() const noexcept { return{}; } }; }; }
When the compiler encounters a function which contains a co_await
or co_return
, it realizes that it’s dealing with a coroutine. It collects the following:
- The return type of the function.
- The type of
*this
, if it’s defined as an instance member function. - The types of the parameters, if any.
It takes all of these types and looks for a coroutine_
specialization that matches it. For example, given
ReturnType FreeFunction(ArgType1& arg1, ArgType2 arg2);
the compiler looks for the type
std::experimental::coroutine_traits< ReturnType, ArgType1&, ArgType2>
For an instance method
ReturnType SomeClass::Member(ArgType1& arg1, ArgType2 arg2) const;
the compiler looks for
std::experimental::coroutine_traits< ReturnType, SomeClass const&, ArgType1&, ArgType2>
In practice, few coroutines care about anything other than the return type, so you will generally see the second and subsequent template type parameters declared but ignored.¹
The specialized coroutine_
must have a nested type called promise_
. This could be defined inline or it could be an alias (via typedef
or using
) for another type define elsewhere.
The promise_
is the promise object that is stored inside the coroutine state. The get_
method is called to create the thing that is returned to the caller of the coroutine. In our case, we want to return a copy of our result_
: The result_
state becomes the way that the coroutine and the caller communicate with each other:
Coroutine state | Caller | |||||
---|---|---|---|---|---|---|
bookkeeping | ||||||
promise |
|
holder |
→ | result_ state |
← | holder |
stack frame |
When the coroutine reaches its co_return
, the compiler calls p.return_value()
with the returned value, which we pass onward to the holder by calling set_value
. (If there is no returned value, then the compiler uses p.return_void()
.)² That will in turn update the state, and the state will release any waiting coroutines, which resumes the caller.
On the other hand, if an exception escapes the coroutine, then the compiler will call p.unhandled_
, and we deal with the exception by stowing it in the holder
. Again, this will update the state, and the state will release any waiting coroutines, which resumes the caller. And when the caller performs an await_result
to obtain the result of the co_await
, our state
object rethrows the exception.
So that’s the magic of how the result of the coroutine (either a return value or an exception) gets transferred from the coroutine back to the awaiter.
Wait, what are these last two method initial_
and final_
? We’ll look at those next time.
¹ You could create fancy coroutines that change their implementation depending on what the parameters are. For example, you might define a marker type like
struct with_sugar_t {}; inline constexpr with_sugar_t with_sugar{};
and then have a special version of the result_
coroutine promise that is used if the coroutine function is declared as
result_holder<int> Nicely(with_sugar_t);
Okay, so I don’t have a really good motivation for this feature, but it does exist.
² It is illegal to have both return_
and return_
in a promise type, even if one of them is removed by SFINAE:
template<typename Dummy = void> std::enable_if_t<std::is_same_v<T, void>, Dummy> return_void() const { holder.set_result(); } template<typename Dummy = void> std::enable_if_t<!std::is_same_v<T, void>, Dummy> return_value(T const& value) const { holder.set_result(value); }
The reason is that in order for the compiler to perform substitution in order to determine which methods are callable, it needs to know what was passed to all of the co_return
statements, so it can try substituting them into return_
‘s parameter and see which ones succeed. But it hasn’t started compiling the coroutine function body yet, so it doesn’t know what to try to substitute for value
.
This is a frustrating limitation because it prevents you from writing a single promise that covers both void
-completing coroutines and value-completing coroutines. You always have to make two types, one for return_
and another for return_
.
Probably, the dependence of coroutine_traits on parameters of a function is needed for the case void-returning asynchronous function. E.g. for something like this:
<code>
It should be possible to write coroutine class and corresponding promise that would allow to use async constructs inside AsyncF. And if calling code would like to wait till the end of AsyncF operation, it can await coroutine object.
<code>
I don’t understand how the return_value() method can be const.
Isn’t the holder.set_result() non-const?
(and ditto for unhandled_exception().)
holder.set_result() is const because the value is stored in a shared result_holder_state object, which is external to the holder itself.