Coroutines - Hello World

Before we examine our whole Hello World coroutine program, go through the various parts step-by-step. These include:

  1. The coroutine Promise
  2. The coroutine Context
  3. The coroutine Future
  4. The coroutine Handle
  5. The coroutine itself
  6. 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;
}