Page 30 - MSDN Magazine, October 2017
P. 30
T const& operator*() const {
return value;
}
T const* operator->() const {
return std::addressof(value); }
It might be that the generator and associated range function are used with number-like objects rather than simple primitives. In that case, you might also want to use the addressof helper, should the number-like object be playing tricks with operator& overloading. And that’s all it takes. My range function now works as expected:
template <typename T>
generator<T> range(T first, T last) {
return{ first, last };
}
int main() {
resume as needed to allow a range-based for loop—or any other loop, for that matter—to direct the progress of the coroutine producing a pull- rather than push-model of data consumption. The generator is in some ways similar to that of the classical generator in Figure 1. The big difference is that rather than updating values directly, it merely nudges the coroutine forward. Figure 2 provides the outline.
So, there’s a little more going on here. Not only is there an iterator that allows the range-based for loop to interact with the generator from the outside, but there’s also a promise_type that allows the coroutine to interact with the generator from the inside. First, some housekeeping: Recall that the function generating values won’t be returning a generator directly, but rather allow a developer to use co_yield statements to forward values from the coroutine, through the generator, and to the call site. Consider the simplest of generators:
generator<int> one_two_three() {
for (int i : range(0, 10)) {}
co_yield 1; co_yield 2; co_yield 3;
printf("%d\n", i); }
Of course, this isn’t particularly flexible. I’ve produced the iota of my dreams, but it’s still just an iota of what would be possible if I switched gears and embraced coroutines. You see, with coroutines you can write all kinds of generators far more succinctly and with- out having to write a new generator class template for each kind of range you’d like to produce. Imagine if you only had to write one more generator and then have an assortment of range-like functions to produce different sequences on demand. That’s what coroutines enable. Instead of embedding the knowledge of the original iota gen- eration into the generator, you can embed that knowledge directly inside the range function and have a single generator class template that provides the glue between producer and consumer. Let’s do it.
I begin by including the coroutine header, which provides the definition of the coroutine_handle class template:
#include <experimental/coroutine>
I’ll use the coroutine_handle to allow the generator to interact with the state machine represented by a coroutine. This will query and
Figure 2 A Coroutine Generator
Notice how the developer never explicitly creates the coroutine return type. That’s the role of the C++ compiler as it stitches together the state machine represented by this code. Essentially, the C++ compiler looks for the promise_type and uses that to con- struct a logical coroutine frame. Don’t worry, the coroutine frame will likely disappear after the C++ compiler is done optimizing the code in some cases. Anyway, the promise_type is then used to initialize the generator that gets returned to the caller. Given the promise_type, I can get the handle representing the coroutine so that the generator can drive it from the outside in:
generator(promise_type& promise) : handle(handle_type::from_promise(promise))
{ }
Of course, the coroutine_handle is a pretty low-level construct and I don’t want a developer holding onto a generator to acciden- tally corrupt the state machine inside of an active coroutine. The solution is simply to implement move semantics and prohibit copies. Something like this (first, I’ll give it a default constructor and expressly delete the special copy members):
generator() = default;
generator(generator const&) = delete;
generator &operator=(generator const&) = delete;
And then I’ll implement move semantics simply by transferring the coroutine’s handle value so that two generators never point to the same running coroutine, as shown in Figure 3.
Figure 3 Implementing Move Semantics
}
template <typename T> struct generator
{
struct promise_type{ ... };
using handle_type = std::experimental::coroutine_handle<promise_type>; handle_type handle{ nullptr };
struct iterator{ ... };
iterator begin() {
... handle.resume(); ...
}
iterator end() {
return nullptr; }
};
generator(generator&& other) : handle(other.handle) {
other.handle = nullptr; }
generator &operator=(generator&& other) {
if (this != &other) {
handle = other.handle;
other.handle = nullptr; }
return *this; }
26 msdn magazine
C++