Understanding C++ Awaitable Objects and Awaiters

In C++20 coroutines, asynchronous execution relies on suspension and resumption mechanisms governed by awaitable types. This article explains what an awaitable object is, how it differs from an awaiter, and the specific requirements for implementing the three mandatory awaiter methods: await_ready, await_suspend, and await_resume.

What Is an Awaitable Object?

In C++, an awaitable object is any type that can be used as the operand of the co_await operator. It represents a computation or operation that might yield control back to the caller before producing a result.

An object becomes awaitable in one of two ways:

  1. Directly Awaitable: The object itself implements the awaiter interface directly.
  2. Indirectly Awaitable: The object defines an operator co_await (as a member or non-member) that returns an awaiter, or the current coroutine's promise_type defines an await_transform member function that converts the object into an awaiter.

The Awaiter Concept

While an "awaitable" is the type passed to co_await, an "awaiter" is the actual object that the C++ runtime interacts with to control suspension and resumption. An awaiter must define three specific member functions:

struct MyAwaiter {
    bool await_ready() noexcept;
    auto await_suspend(std::coroutine_handle<> handle);
    auto await_resume();
};

Required Awaiter Methods

1. await_ready

  • Signature: bool await_ready() [noexcept]
  • Purpose: Determines whether the coroutine needs to suspend at all.
  • Behavior:
    • If it returns true, the operation is already complete. The coroutine skips suspension entirely and proceeds immediately to await_resume().
    • If it returns false, the coroutine pauses execution, preserves its state in the coroutine frame, and transfers execution to await_suspend().

2. await_suspend

  • Signature: auto await_suspend(std::coroutine_handle<Promise> handle)
  • Purpose: Coordinates what happens once the coroutine is suspended. It receives a std::coroutine_handle representing the suspended coroutine, which can be saved to resume the coroutine later (e.g., in an event loop or background thread).
  • Return Types:
    • void: Unconditionally keeps the coroutine suspended and returns control to the caller or resumer of the current coroutine.
    • bool: Conditionally suspends the coroutine. Returning true keeps the coroutine suspended, while returning false immediately resumes it on the current thread.
    • std::coroutine_handle<...>: Performs a symmetric transfer. Control immediately transfers to the returned coroutine handle without consuming additional stack space.

3. await_resume

  • Signature: auto await_resume()
  • Purpose: Produces the final result of the co_await expression.
  • Behavior:
    • This function is invoked when the coroutine resumes, or immediately after await_ready() if await_ready() returned true.
    • The return type of await_resume() becomes the type and value of the entire co_await expression. If the operation yields no value, its return type is void.
    • If the asynchronous operation failed, await_resume() can rethrow an exception, which will propagate out of the co_await expression into the coroutine body.