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

Raymond Chen

Raymond

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.

Propertystructfunction
Instance membersYesYes
Static membersYesNo
Allows parametersYesYes
Different awaitable type
depending on parameter types
NoYes
Different awaitable type
depending on parameter values
NoNo
Warn if not co_awaitedNoYes

(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.

1 comment

Comments are closed. Login to edit/delete your existing comments

  • Johel Ernesto Guerrero Peña
    Johel Ernesto Guerrero Peña

    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”.