Skip to main content
A coroutine in C++ is a function that can suspend execution to be resumed later, maintaining its local state across suspensions. A function becomes a coroutine implicitly if its body contains at least one of three specific keywords: co_await, co_yield, or co_return. Unlike standard functions, coroutines do not use the standard return statement and rely on a compiler-generated state machine to manage execution flow.

Coroutine Keywords

  • co_await <expr>: Evaluates an awaiter object. Suspends the coroutine’s execution only if the awaiter’s await_ready() method returns false. If it returns true, the coroutine continues execution synchronously without suspending.
  • co_yield <expr>: Syntactic sugar for co_await promise.yield_value(<expr>). Suspends the coroutine and returns a value to the caller, provided the resulting awaiter’s await_ready() method returns false.
  • co_return <expr>: Terminates the coroutine and provides a final return value (or void).

Architectural Components

C++ coroutines are stackless and rely on a tripartite architecture consisting of the Coroutine State, the Promise Object, and the Coroutine Handle.

1. Coroutine State

A compiler-generated, typically heap-allocated frame that persists across suspensions. It contains:
  • The promise object.
  • Copied parameters passed to the coroutine.
  • Local variables with a lifetime spanning a suspension point.
  • The current execution state (instruction pointer).

2. The Promise Type

The promise object is manipulated from inside the coroutine. It dictates the coroutine’s behavior, such as initialization, termination, and exception handling. The return type of a coroutine must define a nested promise_type (or specialize std::coroutine_traits).

3. The Coroutine Handle

The std::coroutine_handle<promise_type> is a non-owning pointer manipulated from outside the coroutine. The caller uses it to interact with the suspended coroutine state.

The Awaiter Interface

When co_await <expr> is evaluated, <expr> must resolve to an Awaitable object. The compiler interacts with this object via the Awaiter interface to determine exactly how the suspension occurs.
Note: The standard library provides two trivial awaiters: std::suspend_always (always suspends) and std::suspend_never (never suspends).

Compiler Execution Flow

When a coroutine is invoked, the compiler injects boilerplate to manage the state machine:
  1. Allocates the coroutine state using operator new.
  2. Copies function parameters into the coroutine state.
  3. Constructs the promise_type object.
  4. Calls promise.get_return_object(). The result is kept as a local variable to be returned to the caller.
  5. Begins an implicit try-catch block.
  6. Executes co_await promise.initial_suspend() inside the try-catch block.
  7. Executes the actual body of the coroutine.
  8. If an exception escapes the body or the resume phase of initial_suspend, it is caught and routed to promise.unhandled_exception().
  9. Upon reaching co_return or the end of the body, calls promise.return_void() or promise.return_value().
  10. Destroys local variables in reverse order of creation.
  11. Exits the try-catch block.
  12. Executes co_await promise.final_suspend().
  13. State destruction depends on the behavior of final_suspend:
    • If final_suspend suspends the coroutine (e.g., returns std::suspend_always), the caller or resumer is responsible for calling .destroy() on the handle to deallocate the state.
    • If final_suspend does not suspend (e.g., returns std::suspend_never), the coroutine state is destroyed automatically upon completion. Manually calling .destroy() in this scenario results in undefined behavior (a double-free).

Restrictions

A function cannot be a coroutine if it is:
  • A constexpr function.
  • A constructor or destructor.
  • The main function.
  • A function using standard return statements (a coroutine must exclusively use co_return if it returns).
  • A function with variadic arguments (varargs), such as void foo(int, ...).
  • A function with a placeholder return type (auto or decltype(auto)).
Tired of Poor C++ Skills? Fix That With Deep Grasping!Learn More