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.
Thanks for these articles.
Please, note that in your “Property” table, the only difference should be in “Static members”.
As for “Different awaitable type depending on parameter types”, you show how CTAD can be used in future articles, so “struct” should also be “Yes”.
As for “Warn if not co_awaited”, C++20 allows applying [[nodiscard]] to constructors (as a DR), so again, “struct” should also be “Yes”.