C++ coroutines: Constructible awaitable or function returning awaitable?

Raymond

December 10th, 2019

Last time, we learned how to create simple awaitable objects by creating a structure that implements the await_ suspend method (and relies on suspend_ always to do the coroutine paperwork for us). We can then construct the awaitable object and then co_await on it.

As a reminder, here’s our resume_ new_ thread structure:

struct resume_new_thread : std::experimental::suspend_always { void await_suspend( std::experimental::coroutine_handle<> handle) { std::thread([handle]{ handle(); }).detach(); } };

Another option is to write a function that returns a simple awaitable object, and co_await on the return value.

auto resume_new_thread() { struct awaiter : std::experimental::suspend_always { void await_suspend( std::experimental::coroutine_handle<> handle) { std::thread([handle]{ handle(); }).detach(); } }; return awaiter{}; }

What’s the difference? Which is better?

Both awaitable object patterns let you put instance members on the awaitable object:

auto o = blah(); o.configure_something(true); co_await o; // fluent interface pattern co_await blah().configure_something(true);

In order to have static members, the type must be publicly visible.

// blah can be a struct but not a function co_await blah::fluffy();

Both of the patterns permit the blah to be parameterized:

co_await blah(1, false);

but only the function pattern permits a different awaitable object to be returned based on the parameter types. That’s because the function pattern lets you create a different overloaded function for each set of parameters.

co_await blah(1); // awaits whatever blah(int) returns co_await blah(false); // awaits whatever blah(bool) returns

The function version also supports marking the return value as [[nodiscard]] , which recommends that the compiler issue a warning if the return value is not consumed. This avoids a common mistake of writing

blah();

instead of

co_await blah();

Let’s make a comparison table.

Property struct function Instance members Yes Yes Static members Yes No Allows parameters Yes Yes Different awaitable type

depending on parameter types No Yes Different awaitable type

depending on parameter values No No Warn if not co_await ed No Yes

(Note that neither gives you the ability to change the awaitable type based on the parameter values.)

Here’s a sketch of how each pattern would implement what it can:

struct blah : std::experimental::suspend_always { void await_suspend( std::experimental::coroutine_handle<> handle); // instance member, fluent interface pattern blah& configure_something(bool value); // static member static blah fluffy(); // parameterized blah(); blah(int value); blah(bool value); }; // function pattern [[nodiscard]] auto blah() { struct awaiter : std::experimental::suspend_always { void await_suspend( std::experimental::coroutine_handle<> handle) { ... } // instance member, fluent interface pattern awaiter& configure_something(bool value) { ... } }; return awaiter{}; } [[nodiscard]] auto blah(int value) { struct awaiter : std::experimental::suspend_always { void await_suspend( std::experimental::coroutine_handle<> handle) { ... } // instance member, used only for blah(int) awaiter& configure_int(bool value) { ... } }; return awaiter{}; } [[nodiscard]] auto blah(bool value) { struct awaiter : std::experimental::suspend_always { void await_suspend( std::experimental::coroutine_handle<> handle) { ... } // instance member, used only for blah(bool) awaiter& configure_bool(bool value) { ... } }; return awaiter{}; }

The upside of the function pattern is that you can have completely different implementations depending on which overload is called. The downside is that you end up repeating yourself a lot. Though you may be able to reduce some of the extra typing by factoring into a base class in an implementation namespace.