Part III: Coroutine Machinery
This section explains the promise type and coroutine handle—the machinery that makes coroutines work.
The Promise Type
Every coroutine has an associated promise type that controls its behavior. The compiler finds the promise type through the coroutine’s return type:
struct MyTask
{
struct promise_type { /* ... */ }; // <-- Compiler looks here
// ...
};
MyTask my_coroutine() // Return type determines promise_type
{
co_return;
}
The promise object lives in the coroutine frame and acts as a communication channel between the coroutine and the outside world.
Required Promise Methods
A promise type must provide these methods:
struct promise_type
{
// Create the return object (called immediately)
ReturnType get_return_object();
// Should we suspend at the start?
Awaiter initial_suspend();
// Should we suspend at the end?
Awaiter final_suspend() noexcept;
// Handle co_return; (void coroutines)
void return_void();
// OR handle co_return value; (non-void coroutines)
void return_value(T value);
// Handle exceptions
void unhandled_exception();
};
get_return_object()
Called before the coroutine body runs. Returns the object that the coroutine function call produces:
MyTask get_return_object()
{
return MyTask{
std::coroutine_handle<promise_type>::from_promise(*this)
};
}
This is how the caller gets a handle to interact with the coroutine.
initial_suspend()
Called immediately after the promise is constructed. Returns an awaiter that determines whether the coroutine starts running or suspends immediately:
// Eager: start running immediately
std::suspend_never initial_suspend() { return {}; }
// Lazy: suspend until someone resumes
std::suspend_always initial_suspend() { return {}; }
Lazy coroutines (returning suspend_always) are easier to compose because
you control exactly when they start.
final_suspend()
Called after co_return or when the coroutine body ends. Returns an awaiter:
// Suspend at end: caller must destroy the frame
std::suspend_always final_suspend() noexcept { return {}; }
// Don't suspend: frame is destroyed automatically
std::suspend_never final_suspend() noexcept { return {}; }
If you suspend at final, the caller can access the result before the frame is destroyed. This is required for returning values from coroutines.
final_suspend() must be noexcept. Throwing here is undefined behavior.
|
return_void() / return_value()
Handle co_return statements:
// For coroutines that don't return a value
void return_void() {}
// For coroutines that return a value
void return_value(int value)
{
result_ = value;
}
You provide one or the other, never both. The choice depends on whether your coroutine produces a result.
unhandled_exception()
Called when an exception escapes the coroutine body:
void unhandled_exception()
{
exception_ = std::current_exception(); // Store for later
}
// Or terminate immediately
void unhandled_exception()
{
std::terminate();
}
Storing the exception allows the awaiting coroutine to handle it when it tries to get the result.
Optional Promise Methods
Several optional methods customize behavior:
struct promise_type
{
// Customize operator new for the coroutine frame
void* operator new(std::size_t size);
void operator delete(void* ptr);
// Transform awaited expressions
template<typename T>
auto await_transform(T&& expr);
// Handle co_yield
auto yield_value(T value);
};
await_transform()
Intercepts every co_await expression in the coroutine. This enables:
-
Injecting context (executor, cancellation token) into awaitables
-
Prohibiting certain awaitable types
-
Logging or debugging
template<typename T>
auto await_transform(T&& awaitable)
{
// Wrap to inject our executor
return wrapped_awaitable{std::forward<T>(awaitable), executor_};
}
Coroutine Handle
The std::coroutine_handle<P> is a lightweight handle to a coroutine frame.
It’s similar to a raw pointer—cheap to copy, but doesn’t manage lifetime.
Basic Operations
std::coroutine_handle<promise_type> h;
// Resume the coroutine
h.resume();
h(); // Same as resume()
// Destroy the coroutine frame
h.destroy();
// Check if at final suspension
bool done = h.done();
// Access the promise
promise_type& p = h.promise();
// Get from promise
h = std::coroutine_handle<promise_type>::from_promise(promise);
// Type-erased handle (loses promise access)
std::coroutine_handle<> erased = h;
Handle vs Pointer
Like a raw pointer:
-
Cheap to copy (typically one pointer)
-
Doesn’t own the resource
-
Can be null (default-constructed)
-
Can dangle if frame is destroyed
Unlike a raw pointer:
-
destroy()instead ofdelete -
resume()instead of dereferencing -
Type parameter gives access to promise
coroutine_handle<void>
The type-erased variant std::coroutine_handle<> (alias for
coroutine_handle<void>) can refer to any coroutine:
void resume_any(std::coroutine_handle<> h)
{
h.resume(); // Works for any coroutine
// h.promise() is not available—we don't know the promise type
}
This is useful for schedulers that don’t need promise access.
Noop Coroutine
std::noop_coroutine() returns a handle to a coroutine that does nothing
when resumed:
std::coroutine_handle<> await_suspend(std::coroutine_handle<> h)
{
save_continuation(h);
if (completed_synchronously_)
return h; // Resume immediately
return std::noop_coroutine(); // Don't resume anything
}
This is essential for symmetric transfer patterns.
Putting It Together
Let’s build a complete task<T> type step by step.
Step 1: Basic Structure
template<typename T>
struct task
{
struct promise_type;
std::coroutine_handle<promise_type> handle_;
explicit task(std::coroutine_handle<promise_type> h) : handle_(h) {}
~task()
{
if (handle_)
handle_.destroy();
}
// Move-only
task(task&& other) noexcept
: handle_(std::exchange(other.handle_, nullptr)) {}
task& operator=(task&&) = delete;
task(task const&) = delete;
};
Step 2: Promise Type
template<typename T>
struct task<T>::promise_type
{
std::optional<T> result_;
std::exception_ptr exception_;
task get_return_object()
{
return task{
std::coroutine_handle<promise_type>::from_promise(*this)
};
}
std::suspend_always initial_suspend() { return {}; } // Lazy
std::suspend_always final_suspend() noexcept { return {}; }
void return_value(T value)
{
result_ = std::move(value);
}
void unhandled_exception()
{
exception_ = std::current_exception();
}
};
Step 3: Awaitable Interface
template<typename T>
struct task
{
// ... previous code ...
bool await_ready() const { return false; } // Always suspend
std::coroutine_handle<> await_suspend(std::coroutine_handle<> caller)
{
// Store caller so we can resume them when we complete
handle_.promise().continuation_ = caller;
return handle_; // Start running this task
}
T await_resume()
{
if (handle_.promise().exception_)
std::rethrow_exception(handle_.promise().exception_);
return std::move(*handle_.promise().result_);
}
};
Step 4: Continuation Handling
Update promise to resume the awaiting coroutine:
template<typename T>
struct task<T>::promise_type
{
std::coroutine_handle<> continuation_;
// ... other members ...
auto final_suspend() noexcept
{
struct final_awaiter
{
bool await_ready() noexcept { return false; }
std::coroutine_handle<> await_suspend(
std::coroutine_handle<promise_type> h) noexcept
{
// Resume whoever was waiting for us
return h.promise().continuation_;
}
void await_resume() noexcept {}
};
return final_awaiter{};
}
};
Step 5: Usage
task<int> inner()
{
co_return 42;
}
task<int> outer()
{
int x = co_await inner(); // Suspends, runs inner, resumes with result
co_return x * 2;
}
The flow:
-
outer()creates a suspended task -
Someone resumes
outer -
outerhitsco_await inner() -
outersuspends, storing itself as continuation -
innerruns, returns 42 -
innerat final_suspend resumesouter -
outerresumes withx = 42 -
outerreturns 84
Building a Generator
Generators use co_yield to produce sequences:
template<typename T>
struct generator
{
struct promise_type
{
T current_value_;
std::exception_ptr exception_;
generator get_return_object()
{
return generator{
std::coroutine_handle<promise_type>::from_promise(*this)
};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
std::suspend_always yield_value(T value)
{
current_value_ = std::move(value);
return {};
}
void unhandled_exception()
{
exception_ = std::current_exception();
}
};
std::coroutine_handle<promise_type> handle_;
// Iterator interface for range-for
struct iterator { /* ... */ };
iterator begin();
iterator end();
};
Usage:
generator<int> iota(int start)
{
while (true)
co_yield start++;
}
for (int x : iota(0))
{
if (x > 10) break;
std::cout << x << "\n";
}
Summary
| Component | Role |
|---|---|
Promise type |
Controls coroutine behavior, stores results |
|
Creates the caller-visible return value |
|
Eager vs lazy execution |
|
Cleanup and continuation |
|
Lightweight reference to coroutine frame |
|
Intercept and transform |
Next Steps
The basics are in place. Now learn the advanced techniques:
-
Part IV: Advanced Topics — Symmetric transfer, allocation, exceptions