The Framework

Understand the relation between promise object, coroutine handle, and coroutine frame.

We'll cover the following...

The framework for implementing coroutines consists of more than 2020 functions, some of which you must implement and some of which you may overwrite. Therefore, you can tailor the coroutine to your needs.

A coroutine is associated with three parts: the promise object, the coroutine handle, and the coroutine frame. The client gets the coroutine handle to interact with the promise object, which keeps its state in the coroutine frame.

Promise object

The promise object is manipulated from inside the coroutine. It delivers its result or exception via the promise object.

The promise object must support the following interface.

Member function Description
Default constructor A promise must be default constructible.
initial_suspend() Determines if the coroutine suspends before it runs.
final_suspend() Determines if the coroutine suspends before it ends.
unhandled_exception() Called when an exception happens.
get_return_object() Returns the coroutine object (resumable object).
return_value(val) Is invoked by co_return val.
return_void() Is invoked by co_return.
yield_value(val) Is invoked by co_yield val.

The compiler automatically invokes these functions during its execution of the coroutine. The section workflow presents this workflow in detail.

The function get_return_object returns a resumable object that the client gets to interact with the coroutine. A promise needs at least one of the member functions return_value, return_void, or yield_value. You don’t need to define the member functions return_value or return_void if your coroutines never ends.

The three functions yield_value, initial_suspend, and final_suspend return awaitables. An awaitable is something that you can await on. The awaitable determines if the coroutine pauses or not.

Coroutine handle

The coroutine handle is a non-owning handle to resume or destroy the coroutine frame from the outside. The coroutine handle is part of the resumable function.

The following code snippet shows a simple Generator having a coroutine handle coro.

C++
template<typename T>
struct Generator {
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;
Generator(handle_type h): coro(h) {}
handle_type coro;
~Generator() {
if ( coro ) coro.destroy();
}
T getValue() {
return coro.promise().current_value;
}
bool next() {
coro.resume();
return not coro.done();
}
...
}

The constructor (line 7) gets the coroutine handle to the promise that has type std::coroutine_handle<promise_type>. The member functions next (line 15) and getValue (line 12) enables a client to resume the promise (gen.next()) or ask for its value (gen.getValue()) using the coroutine handle.

C++
Generator<int> coroutineFactory(); // function that returns a coroutine object
auto gen = coroutineFactory(); gen.next();
auto result = gen.getValue();

Internally, both functions trigger the coroutine handle coro (line 7) to

  • resume the coroutine: coro.resume() (line 16) or coro();
  • destroy the coroutine: coro.destroy() (line 10);
  • check the state of the coroutine: coro (line 10).

The coroutine is automatically destroyed when its function body ends. The call, coro, only returns true at its final suspend point.

⚠️ The resumable object requires an inner type promise_type

A resumable object such as Generator must have an inner type promise_type. Alternatively, you can specialize std::coroutine_traits on Generator and define a public member promise_type in it: std::coroutine_traits<Generator>.

Coroutine frame

The coroutine frame is an internal, typically heap-allocated state. It consists of a promise object, the coroutine’s copied parameters, the representation of its suspension points, and local variables. Local variables within the coroutine frame are either whose lifetime ends before the current suspension point or those whose lifetime exceeds the current suspension point. Two requirements are necessary to optimize out the allocation of the coroutine:

  1. The lifetime of the coroutine has to be nested inside the lifetime of the caller.
  2. The caller of the coroutine knows the size of the coroutine frame.

The crucial abstraction in the coroutine framework are awaitables and awaiters.