C++ Coroutines Primer (3)

In the previous two articles of this mini-series we introduced co_await, co_yield and co_return, the last of these not taking a parameter. In this article we’ll look at the possibility of co_return actually returning a value, and the minimal boilerplate necessary to allow this functionality to work. Note that this boilerplate does become closer in complexity to Generator.hpp from the first article, so in your own projects you may prefer to start with that source file and adapt it as necessary.

A minimal coroutine function which uses co_return could look like this:

CoroCtx<int> answer(int n) {
    co_return n + 1;
}

We would expect its client code to look something like:

    auto a = answer(41);
    std::cout << "answer(41) is: " << a() << '\n';

Using names from the previous ariticles, clearly we need a CoroCtx, this time with a template parameter being the same type as used with co_return; we do not, however, need an AwaitCtx. The changes to our previous CoroCtx are as follows:

  • The nested struct promise_type needs to have a member variable of type T (being of type int in our example), we’ve used the name value
  • get_return_object() no longer returns a default-constructed object, we need to synthesise the return value from *this
  • final_suspend() returns std::suspend_always, to ensure that the “promise object” is not destroyed too early
  • A new member function return_value(), taking a single parameter which is the same as that used with co_return, sets value (this is all it needs to do, its own actual return type being void)
  • return_void() is no longer needed, in fact it is a compile-time error to have both return_value() and return_void()
  • The outer class CoroCtx also has a member variable h of type std::coroutine_handle<promise_type>
  • There is no default-constructor, instead one which resets h
  • There is a destructor which destroys the current coroutine handle
  • The “function call” operator() is overloaded to return the promise object’s value field; another function, called get() for example, could be used instead

The modified CoroCtx is shown in full below, which should work equally well with T being any built-in or user-defined type:

template<typename T>
struct CoroCtx {
    struct promise_type {
        T value;
        CoroCtx get_return_object() {
              return CoroCtx<T>(std::coroutine_handle<promise_type>::from_promise(*this));
        }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void unhandled_exception() {}
        template<std::convertible_to<T> Return>
        void return_value(Return &&ret) {
            value = std::forward<Return>(ret);
        }
    };

    std::coroutine_handle<promise_type> h;
    CoroCtx(std::coroutine_handle<promise_type> h) : h(h) {}
    ~CoroCtx() { h.destroy(); }
    T operator()() {
        return std::move(h.promise().value);
    }
};

Output from the program is:

answer(41) is: 42

Due to the fact that std::move() is used to return the actual value, a() should only be invoked once. Also, we haven’t covered exception handling and propagation in this simple example, which of course would be desirable in production code.

So, to round up this mini series, we ask the question “Are coroutines useful, or even necessary?” Personally, I’m still sitting on the fence; potential use cases may include:

  • Using coroutine generators (and co_yield) with C++ ranges, rather than needing a stateful lambda function to do the generating
  • Using co_await in the context of “event-loop” programming
  • Using co_return with a value possibly computed by a separate thread to provide more optimal “lazy” evaluation

As with all features new to C++, it may take a while for a common style to develop, and programs with non-linear execution are more difficult to comprehend. So, as always, feel free to experiment, and comment well.

Resources

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s