Page 31 - MSDN Magazine, October 2017
P. 31

Now, given the fact that the coroutine is being driven from the outside, it’s important to remember that the generator also has the responsibility of destroying the coroutine:
~generator() {
if (handle) {
handle.destroy(); }
}
This actually has more to do with the result of final_suspend on the promise_type, but I’ll save that for another day. That’s enough bookkeeping for now. Let’s now look at the generator’s promise_type. The promise_type is a convenient place to park any state such that it will be included in any allocation made for the coroutine frame by the C++ compiler. The generator is then just a lightweight object that can easily move around and refer back to that state as needed. There are only two pieces of information that I really need to convey from within the coroutine back out to the caller. The first is the value to yield and the second is any exception that might have been thrown:
#include <variant>
template <typename T> struct generator
{
struct promise_type {
std::variant<T const*, std::exception_ptr> value;
Although optional, I tend to wrap exception_ptr objects inside std::optional because the implementation of exception_ptr in Visual C++ is a little expensive. Even an empty exception_ptr calls into the CRT during both construction and destruction. Wrapping it inside optional neatly avoids that overhead. A more precise state model is to use a variant, as I just illustrated, to hold either the cur- rent value or the exception_ptr because they’re mutually exclusive. The current value is merely a pointer to the value being yielded inside the coroutine. This is safe to do because the coroutine will be suspended while the value is read and whatever temporary object may be yielded up will be safely preserved while the value is being observed outside of the coroutine.
When a coroutine initially returns to its caller, it asks the promise_type to produce the return value. Because the generator can be constructed by giving a reference to the promise_type, I can simply return that reference here:
promise_type& get_return_object() {
return *this; }
A coroutine producing a generator isn’t your typical concurrency- enabling coroutine and it’s often just the generator that dictates the lifetime and execution of the coroutine. As such, I indicate to the C++ compiler that the coroutine must be initially suspended so that the generator can control stepping through the coroutine, so to speak:
std::experimental::suspend_always initial_suspend() {
return {}; }
Likewise, I indicate that the coroutine will be suspended upon return, rather than having the coroutine destroy itself automatically:
std::experimental::suspend_always final_suspend() {
return {}; }
Figure 4 The Inspirational Range Generator
template <typename T>
generator<int> range(T first, T last) {
while (first != last) {
co_yield first++; }
}
int main() {
for (int i : range(0, 10)) {
printf("%d\n", i); }
}
msdnmagazine.com
October 2017 27
This ensures that I can still query the state of the coroutine, via the promise_type allocated within the coroutine frame, after the coroutine completes. This is essential to allow me to read the exception_ptr upon failure, or even just to know that the corou- tine is done. If the coroutine automatically destroys itself when it completes, I wouldn’t even be able to query the coroutine_handle, let alone the promise_type, following a call to resume the corou- tine at its final suspension point. Capturing the value to yield is now quite straightforward:
std::experimental::suspend_always yield_value(T const& other) {
value = std::addressof(other); return {};
I simply use the handy addressof helper again. A promise_type must also provide a return_void or return_value function. Even though it isn’t used in this example, it hints at the fact that co_yield is really just an abstraction over co_await:
void return_void() {
}
More on that later. Next, I’ll add a little defense against misuse just to make it easier for the developer to figure out what went wrong. You see, a generator yielding values implies that unless the coroutine completes, a value is available to be read. If a coroutine were to include a co_await expression, then it could conceivably suspend without a value being present and there would be no way to convey this fact to the caller. For that reason, I prevent a developer from writing a co_await statement, as follows:
template <typename Expression>
Expression&& await_transform(Expression&& expression) {
static_assert(sizeof(expression) == 0,
"co_await is not supported in coroutines of type generator");
return std::forward<Expression>(expression); }
Wrapping up the promise_type, I just need to take care of catching, so to speak, any exception that might have been thrown. The C++ compiler will ensure that the promise_type’s unhandled_exception member is called:
void unhandled_exception() {
value = std::current_exception(); }
I can then, just as a convenience to the implementation, provide a handy function for optionally rethrowing the exception in the appropriate context:
}


































































































   29   30   31   32   33