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 typeT
(being of typeint
in our example), we’ve used the namevalue
get_return_object()
no longer returns a default-constructed object, we need to synthesise the return value from*this
final_suspend()
returnsstd::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 withco_return
, setsvalue
(this is all it needs to do, its own actual return type beingvoid
) return_void()
is no longer needed, in fact it is a compile-time error to have bothreturn_value()
andreturn_void()
- The outer class
CoroCtx
also has a member variableh
of typestd::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’svalue
field; another function, calledget()
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
- Example code above on GitHub