Coroutines - Hello World
Before we examine our whole Hello World coroutine program, go through the various parts step-by-step. These include:
- The coroutine Promise
- The coroutine Context
- The coroutine Future
- The coroutine Handle
- The coroutine itself
- The subroutine that actually uses the coroutine
The entire file is included at the end of this post.
The Coroutine
Future f()
{
co_return 42;
}
We instantiate our coroutine with
Future myFuture = f();
This is a simple coroutine that just returns the value 42
. It is a coroutine
because it includes the keyword co_return
. Any function that has the keywords
co_await
, co_return
or co_yield
is a coroutine.
The first thing you will notice is that although we are returning an integer, the coroutine return type is (a user defined) type Future. The reason is that when we call our coroutine, we don’t run the function right now, rather we initialize an object which will eventually get us the value we are looking for AKA our Future.
Finding the Promised Type
When we instantiate our coroutine, the first thing the compiler does is find the promise type that represents this particular type of coroutine.
We tell the compiler what promise type belongs to what coroutine function signature by creating a template partial specialization for
template <typename R, typename P...>
struct coroutine_trait
{};
with a member called `promise_type` that defines our Promise Type
For our example we might want to use something like:
template<>
struct std::experimental::coroutines_v1::coroutine_traits<Future> {
using promise_type = Promise;
};
Here we create a specialization of coroutine_trait
specifies no parameters and
a return type Future
, this exactly matches our coroutine function signature of
Future f(void)
. promise_type
is then the promise type which in our case is
the struct Promise
.
Now are a user, we normally will not create our own coroutine_trait
specialization since the coroutine library provides a nice simple way to
specify the promise_type
in the Future class itself. More on that later.
The Coroutine Context
As mentioned in my previous post, because coroutines are suspend-able and resume-able, local variables cannot always be stored in the stack. To store non-stack-safe local variables, the compiler will allocate a Context object on the heap. An instance of our Promise will be stored as well.
The Promise, the Future and the Handle
Coroutines are mostly useless unless they are able to communicate with the outside world. Our promise tells us how the coroutine should behave while our future object allow other code to interact with the coroutine. The Promise and Future then communicate with each-other via our coroutine handle.
The Promise
A simple coroutine promise looks something like:
struct Promise
{
Promise() : val (-1), done (false) {}
std::experimental::coroutines_v1::suspend_never initial_suspend() { return {}; }
std::experimental::coroutines_v1::suspend_always final_suspend() {
this->done = true;
return {};
}
Future get_return_object();
void unhandled_exception() { abort(); }
void return_value(int val) {
this->val = val;
}
int val;
bool done;
};
Future Promise::get_return_object()
{
return Future { Handle::from_promise(*this) };
}
As mentioned, the promise is allocate when the coroutine is instantiated and exits for the entire lifetime of the coroutine.
Once done, the compiler calls get_return_object
This user defined function is
then responsible for creating the Future object and returning it to the
coroutine instatiator.
In our instance, we want our Future to be able to communicate with our coroutine so we create our Future with the handle for our coroutine. This will allow our Future to access our Promise.
Once our coroutine is created, we need to know whether we want to start running
it immediately or whether we want it to remain suspended immediately. This is
done by calling the Promise::initial_suspend()
function. This function returns
an Awaiter which we will look in another post.
In our case since we do want the function to start immediately, we call
suspend_never
. If we suspended the function, we would need to start the
coroutine by calling the resume method on the handle.
We need to know what to do when the co_return
operator is called in
the coroutine. This is done via the return_value
function. In this case we
store the value in the Promise for later retrieval via the Future.
In the event of an exception we need to know what to do. This is done by the
unhandled_exception
function. Since in our example, exceptions should not
occur, we just abort.
Finally, we need to know what to do before we destroy our coroutine. This is
done via the final_suspend function
In this case, since we want to retrieve
the result so we return suspend_always
. The coroutine must then be destroyed
via the coroutine handle destroy
method. Otherwise, if we return
suspend_never
the coroutine destroys itself as soon as it finishes running.
The Handle
The handle give access to the coroutine as well as its promise. There are two flavours, the void handle when we do not need to access the promise and the coroutine handle with the promise type for when we need to access the promise.
template <typename _Promise = void>
class coroutine_handle;
template <>
class _coroutine_handle<void> {
public:
void operator()() { resume(); }
//resumes a suspended coroutine
void resume();
//destroys a suspended coroutine
void destroy();
//determines whether the coroutine is finished
bool done() const;
};
template <Promise>
class coroutine_handle : public coroutine_handle<void>
{
//gets the promise from the handle
Promise& promise() const;
//gets the handle from the promise
static coroutine_handle from_promise(Promise& promise) no_except;
};
The Future
The future looks like this:
class [[nodiscard]] Future
{
public:
explicit Future(Handle handle)
: m_handle (handle)
{}
~Future() {
if (m_handle) {
m_handle.destroy();
}
}
using promise_type = Promise;
int operator()();
private:
Handle m_handle;
};
int Future::operator()()
{
if (m_handle && m_handle.promise().done) {
return m_handle.promise().val;
} else {
return -1;
}
}
The Future object is responsible for abstracting the coroutine to the outside
world. We have a constructor that takes the handle from the promise as per the
promise’s get_return_object
implementation.
The destructor destroys the coroutine since in our case it is the future that control’s the promise’s lifetime.
lastly we have the line:
using promise_type = Promise;
The C++ library saves us from implementing our own coroutine_trait
as we did
above if we define our promise_type
in the return class of the coroutine.
And there we have it. Our very first simple coroutine.
Full Source
#include <experimental/coroutine>
#include <iostream>
struct Promise;
class Future;
using Handle = std::experimental::coroutines_v1::coroutine_handle<Promise>;
struct Promise
{
Promise() : val (-1), done (false) {}
std::experimental::coroutines_v1::suspend_never initial_suspend() { return {}; }
std::experimental::coroutines_v1::suspend_always final_suspend() {
this->done = true;
return {};
}
Future get_return_object();
void unhandled_exception() { abort(); }
void return_value(int val) {
this->val = val;
}
int val;
bool done;
};
class [[nodiscard]] Future
{
public:
explicit Future(Handle handle)
: m_handle (handle)
{}
~Future() {
if (m_handle) {
m_handle.destroy();
}
}
using promise_type = Promise;
int operator()();
private:
Handle m_handle;
};
Future Promise::get_return_object()
{
return Future { Handle::from_promise(*this) };
}
int Future::operator()()
{
if (m_handle && m_handle.promise().done) {
return m_handle.promise().val;
} else {
return -1;
}
}
//The Co-routine
Future f()
{
co_return 42;
}
int main()
{
Future myFuture = f();
std::cout << "The value of myFuture is " << myFuture() << std::endl;
return 0;
}