Having gotten something working, we still have a small problem. We have a coroutine that we can start, and we can choose when to suspend it, but we can't yet resume it.
The coroutines TS describes a class std::experimental::coroutine_handle
which is our interface to the coroutine itself. It's a template which is supposed to be told the promise_type
we're using.
We can add the following to our sync
class:
struct promise_type; using handle_type = std::experimental::coroutine_handle<promise_type>;
We should store the handle in the sync
object, so we can replace our default constructor with this:
sync(handle_type h) : coro(h) { std::cout << "Created a sync object" << std::endl; } handle_type coro;
Now in our promise_type
we will need to pass the correct handle when we create the sync
object. The coroutine_handle
has a static member that allows us to do this:
auto get_return_object() { std::cout << "Send back a sync" << std::endl; return sync<T>{handle_type::from_promise(*this)}; }
Now that we have that we should change our shared_ptr
members. In the promise_type
we'll replace ptr
with with a plain T
member called value
, and we can now remove it totally from our sync
object. This does mean our sync::get
member needs to pull the value from the coroutine_handle
, but there's an API for that:
T get() { std::cout << "We got asked for the return value..." << std::endl; return coro.promise().value; }
We clearly also have to change the way we record the return value in the promise_type
:
auto return_value(T v) { std::cout << "Got an answer of " << v << std::endl; value = v; return std::experimental::suspend_never{}; }
If we try to run this we'll find we still have a problem with the lifetimes. The sync
instance still outlives our promise_type
instance so we hit undefined behaviour. The lifetime issue can be solved by suspending before we run off the end of the coroutine:
auto final_suspend() { std::cout << "Finished the coro" << std::endl; return std::experimental::suspend_always{}; }
I've rather arbitrarily changed the final_suspend
member here, but you could also change return_value
. Just so long as the value
in the promise_type
gets filled in before we suspend we'll be fine.
Now it leaks again of course, but we can plug the leak by destroying the coroutine through its handle in the sync
destructor:
~sync() { std::cout << "Sync gone" << std::endl; if ( coro ) coro.destroy(); }
Now everything lines up and it all works, although there is still a subtle problem. If we copy the sync
object around then we're going to end up destroying the coroutine_handle
from more than one place.
When we deal with a coroutine_handle
we have to remember that this is a resource and needs to be managed properly. The simplest way to deal with this is to make our sync
movable but not copyable.
sync(const sync &) = delete; sync(sync &&s) : coro(s.coro) { std::cout << "Sync moved leaving behind a husk" << std::endl; s.coro = nullptr; } ~sync() { std::cout << "Sync gone" << std::endl; if ( coro ) coro.destroy(); } sync &operator = (const sync &) = delete; sync &operator = (sync &&s) { coro = s.coro; s.coro = nullptr; return *this; }
This final version deals correctly with all lifetime issues and outputs something along these lines:
Promise created Send back a sync Created a sync object Started the coroutine, don't stop now! Thinking deep thoughts… Got an answer of 42 Finished the coro Sync gone Got a coroutine, let's get a value We got asked for the return value… And the coroutine value is: 42 Sync gone Promise died
We now have a working coroutine that executes its body when it's entered. Let's instead see if we can make a lazy coroutine that doesn't execute its body until it gets asked for the value.
Our general strategy is going to be this:
lazy
instance that will co-ordinate the return value.lazy
object is asked for, resume the coroutine.co_return
.lazy
object will return the value.lazy
is destructed.There aren't actually that many changes we need to make. First of all just duplicate the sync
class and call it lazy
—we'll refactor this at the end.
Just to check nothing boneheaded happened first alter answer
to return a lazy<int>
rather than the sync
and make sure we observe the same behaviour.
In order to delay the execution of the coroutine body we need to return suspend_always
from initial_suspend
:
auto initial_suspend() { std::cout << "Started the coroutine, put the brakes on!" << std::endl; return std::experimental::suspend_always{}; }
Now when we run it the coroutine body is never run so we don't see the proper value.
Promise created Send back a lazy Created a lazy object Started the coroutine, put the brakes on! Lazy gone Got a coroutine, let's get a value We got asked for the return value… And the coroutine value is: 0 Lazy gone Promise died
Luckily the coroutine_handle
has a member that allows us to resume the execution of the coroutine at a time of our choosing.
T get() { std::cout << "We got asked for the return value..." << std::endl; coro.resume(); return coro.promise().value; }
With this additional change we now get the right result again, and we can see that things happen when we wanted them to:
Promise created Send back a lazy Created a lazy object Started the coroutine, put the brakes on! Lazy gone Got a coroutine, let's get a value We got asked for the return value… Thinking deep thoughts… Got an answer of 42 Finished the coro And the coroutine value is: 42 Lazy gone Promise died
This describes the general pattern when working with coroutines. The way that the coroutine works in detail is fully described by the way that its return type controls the coroutine machinery provided the language. This has the unfortunate side effect of rendering useless auto
type deduction for the return types. Oh well.
Notice how in both our sync
and lazy
examples the body of answer
not changed at all. In both cases the literal 42 is returned (an int
), but we wrap in something that controls how that value finds it way to the caller of answer
.
Now that we understand that, we can refactor our code to remove the redundancy and highlight the important differences between the two implementations.
With that done it's easy to see that the promise types for two differ in whether they suspend or not at the initial suspend point, they also (obviously) differ in the types of object returned by get_return_object
. And the final place they differ is in the get
implementation which for lazy
must also resume the coroutine:
template<typename T> struct sync : public coreturn<T> { using coreturn<T>::coreturn; using handle_type = typename coreturn<T>::handle_type; T get() { std::cout << "We got asked for the return value..." << std::endl; return coreturn<T>::get(); } struct promise_type : public coreturn<T>::promise { auto get_return_object() { std::cout << "Send back a sync" << std::endl; return sync<T>{handle_type::from_promise(*this)}; } auto initial_suspend() { std::cout << "Started the coroutine, don't stop now!" << std::endl; return std::experimental::suspend_never{}; } }; }; template<typename T> struct lazy : public coreturn<T> { using coreturn<T>::coreturn; using handle_type = typename coreturn<T>::handle_type;; T get() { std::cout << "We got asked for the return value..." << std::endl; this->coro.resume(); return coreturn<T>::get(); } struct promise_type : public coreturn<T>::promise { auto get_return_object() { std::cout << "Send back a lazy" << std::endl; return lazy<T>{handle_type::from_promise(*this)}; } auto initial_suspend() { std::cout << "Started the coroutine, put the brakes on!" << std::endl; return std::experimental::suspend_always{}; } }; };
The common part is now:
template<typename T> struct coreturn { struct promise; friend struct promise; using handle_type = std::experimental::coroutine_handle<promise>; coreturn(const coreturn &) = delete; coreturn(coreturn &&s) : coro(s.coro) { std::cout << "Coreturn wrapper moved" << std::endl; s.coro = nullptr; } ~coreturn() { std::cout << "Coreturn wrapper gone" << std::endl; if ( coro ) coro.destroy(); } coreturn &operator = (const coreturn &) = delete; coreturn &operator = (coreturn &&s) { coro = s.coro; s.coro = nullptr; return *this; } struct promise { friend struct coreturn; promise() { std::cout << "Promise created" << std::endl; } ~promise() { std::cout << "Promise died" << std::endl; } auto return_value(T v) { std::cout << "Got an answer of " << v << std::endl; value = v; return std::experimental::suspend_never{}; } auto final_suspend() { std::cout << "Finished the coro" << std::endl; return std::experimental::suspend_always{}; } void unhandled_exception() { std::exit(1); } private: T value; }; protected: T get() { return coro.promise().value; } coreturn(handle_type h) : coro(h) { std::cout << "Created a coreturn wrapper object" << std::endl; } handle_type coro; };
There is one final thing we need to think about. What happens if we fetch the answer from the coroutine more than once? We can change main
like this:
int main() { std::cout << "coro1" << std::endl; auto a = answer(); std::cout << "Got a coroutine, let's get a value" << std::endl; auto v = a.get(); std::cout << "And the coroutine value is: " << v << std::endl; v = a.get(); std::cout << "And the coroutine value is still: " << v << std::endl; return 0; }
On my machine I get this output:
Promise created Send back a lazy Created a coreturn wrapper object Started the coroutine, put the brakes on! Coreturn wrapper moved Coreturn wrapper gone Got a coroutine, let's get a value We got asked for the return value… Thinking deep thoughts… Got an answer of 42 Finished the coro And the coroutine value is: 42 We got asked for the return value…
And then it crashes. What happens of course is that we resume a coroutine which is already at its final suspension point, which means we run off the end of it and it's automatically destroyed for us. This ends badly.
What we need to do is make sure we only resume the coroutine the first time get
is called. Luckily there is a simple way to do this:
T get() { std::cout << "We got asked for the return value..." << std::endl; if ( not this->coro.done() ) this->coro.resume(); return coreturn<T>::get(); }
done
will return true
only when the coroutine is at its final suspension point (the one after the body is exited). With that change we have a nice reliable lazy function that will only ever be evaluated once.
Next time we're going to extend this further and start to look at the co_yield
mechanism which will allow us to build some generators.