C++ Coroutine Handles and Promises Explained
In C++20, coroutines introduce cooperative multitasking by allowing functions to suspend execution and resume later without losing their state. This mechanism is primarily governed by two components: the promise object, which controls the coroutine from the inside and manages state and return values, and the coroutine handle, a non-owning pointer-like object used from the outside to resume or destroy the coroutine. Together, they coordinate the allocation, state management, suspension points, and final destruction of the coroutine frame.
The Coroutine Frame
When a function containing co_await,
co_yield, or co_return is invoked, the
compiler allocates a coroutine state (commonly called the coroutine
frame) on the heap. This frame stores:
- The promise object.
- The function arguments passed by value.
- Local variables spanning suspension points.
- The current suspension point to track where execution should resume.
The Promise Object: Internal State Management
The promise object dictates how a coroutine behaves internally. The
compiler deduces the promise type from the coroutine's return type using
the std::coroutine_traits template, typically expecting an
inner type named promise_type.
The promise controls the lifecycle through specific customization points:
get_return_object(): Called first when the coroutine is invoked. It creates the object returned to the caller (such as a task, generator, or future).initial_suspend(): Evaluated before the coroutine body runs. Returningstd::suspend_alwayskeeps the coroutine paused initially (lazy execution), whilestd::suspend_neverstarts execution immediately (eager execution).final_suspend()noexcept: Evaluated after the coroutine finishes execution. Returningstd::suspend_alwaysleaves the coroutine frame intact so the caller can read results;std::suspend_neverdestroys it automatically.yield_value(val): Invoked byco_yield val. It stores the produced value in the promise and returns an awaiter (usuallystd::suspend_always).return_value(val)orreturn_void(): Invoked byco_return. It stores the final result or signals completion.unhandled_exception(): Invoked if an exception escapes the coroutine body, allowing the promise to store the exception pointer viastd::current_exception().
The Coroutine Handle: External Execution Control
The coroutine handle
(std::coroutine_handle<Promise>) is a lightweight,
non-owning handle pointing directly to the underlying coroutine frame.
It acts as the driver used by callers, schedulers, or event loops to
manipulate execution.
Key capabilities of std::coroutine_handle include:
resume()(oroperator()): Resumes execution of a suspended coroutine from its last suspension point.done(): Returnstrueif the coroutine has reached its final suspension point or finished.destroy(): Manually deallocates the coroutine frame. This is essential iffinal_suspend()returnedstd::suspend_always, preventing memory leaks.promise(): Accesses a reference to the associated promise object.from_promise(Promise&): Static method that obtains the coroutine handle corresponding to a given promise instance.- Type-erasure
(
std::coroutine_handle<void>): A specialized handle type that can hold any coroutine handle regardless of its promise type, ideal for integration with schedulers and executors.
How Promises and Handles Coordinate Suspension
The interaction between the handle and promise coordinates suspension and resumption across execution boundaries:
- Initialization: The caller calls the coroutine. The
compiler allocates the frame and initializes the promise. The promise
creates the return object via
get_return_object(), often capturing its own handle viastd::coroutine_handle<promise_type>::from_promise(*this)to embed it inside the return object. - Suspension: When a
co_awaitorco_yieldexpression triggers suspension, the coroutine saves local variables to the frame and pauses. Control returns to the caller or event loop, which retains the coroutine handle. - Communication: Data produced by the coroutine is
written to the promise object during suspension (via
yield_valueor internal awaiter logic). The caller reads this data by querying the promise throughhandle.promise(). - Resumption: When the external system is ready for
more data or an asynchronous operation completes, it invokes
handle.resume(). The execution resumes right after the suspension point. - Teardown: Once the coroutine reaches
co_return,final_suspend()is evaluated. If suspended at the end, the external owner callshandle.destroy()to free the frame and finalize state cleanup.