| name | cpp-coroutines |
| description | C++20 coroutines skill for understanding coroutine mechanics and debugging. Use when working with co_await, co_yield, co_return, implementing promise_type, understanding coroutine frame layout, debugging suspended coroutines in GDB, or inspecting frame allocation with Compiler Explorer. Activates on queries about C++20 coroutines, co_await, co_yield, promise_type, coroutine_handle, coroutine suspension, or coroutine frame. |
C++20 Coroutines
Purpose
Guide agents through C++20 coroutine mechanics: co_await, co_yield, co_return, implementing the required promise_type, understanding coroutine frame memory layout, debugging suspended coroutines in GDB, and reducing frame allocation overhead.
Triggers
- "How do co_await, co_yield, and co_return work?"
- "How do I implement promise_type for a coroutine?"
- "How does a coroutine suspend and resume?"
- "How do I debug a suspended coroutine in GDB?"
- "How much memory does a coroutine frame use?"
- "How do I write a generator with co_yield?"
Workflow
1. The three coroutine keywords
co_return value;
co_yield value;
auto result = co_await some_awaitable;
A function is a coroutine if it contains any of these three keywords. Its return type must be a coroutine type with a promise_type.
2. Minimal coroutine type — Task
#include <coroutine>
#include <stdexcept>
#include <optional>
template <typename T>
struct Task {
struct promise_type {
std::optional<T> value;
std::exception_ptr exception;
Task get_return_object() {
return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_value(T v) { value = std::move(v); }
void unhandled_exception() { exception = std::current_exception(); }
};
std::coroutine_handle<promise_type> handle;
explicit Task(std::coroutine_handle<promise_type> h) : handle(h) {}
Task(Task&&) = default;
Task& operator=(Task&&) = default;
~Task() { if (handle) handle.(); }
{
handle.();
(handle.().exception)
std::(handle.().exception);
std::(*handle.().value);
}
};
{
;
}
{
task = ();
result = task.();
}
3. Generator with co_yield
template <typename T>
struct Generator {
struct promise_type {
T current_value;
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() {}
void unhandled_exception() { throw; }
std::suspend_always yield_value(T value) {
current_value = value;
return {};
}
};
std::coroutine_handle<promise_type> handle;
explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
~Generator() { if (handle) handle.destroy(); }
struct iterator {
std::coroutine_handle<promise_type> handle;
bool done;
iterator& operator++() {
handle.resume();
done = handle.();
*;
}
T *() { handle.().current_value; }
!=(std::) { !done; }
};
{
handle.();
{handle, handle.()};
}
{ {}; }
};
{
( i = start; i < end; ++i)
i;
}
( x : (, )) {
std::cout << x << ;
}
4. Awaitable — custom co_await target
struct TimerAwaitable {
int delay_ms;
bool await_ready() const noexcept { return delay_ms <= 0; }
void await_suspend(std::coroutine_handle<> h) {
std::thread([h, this]() {
std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms));
h.resume();
}).detach();
}
void await_resume() const noexcept {}
};
std::suspend_always{};
std::suspend_never{};
5. Coroutine frame layout and memory
The compiler allocates a coroutine frame (heap object) containing:
- Local variables that live across suspension points
- The promise object
- The current suspension state (where to resume)
- A pointer to the resumption/destruction functions
struct Bad {
std::vector<char> large_buf;
co_return large_buf.size();
};
std::vector<char> buf = get_data();
size_t sz = buf.size();
buf.clear();
co_await next_event;
6. Debugging suspended coroutines in GDB
(gdb) info locals
(gdb) p *(promise_type*)(handle.__handle_)
(gdb) info coroutines
(gdb) step
(gdb) finish
(gdb) next
(gdb) info threads
(gdb) thread 2
(gdb) bt
7. Boost.Asio co_spawn and co_await
#include <boost/asio.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/awaitable.hpp>
namespace net = boost::asio;
net::awaitable<void> echo_session(net::ip::tcp::socket socket) {
char buf[1024];
for (;;) {
std::size_t n = co_await socket.async_read_some(net::buffer(buf));
co_await net::async_write(socket, net::buffer(buf, n));
}
}
int main() {
net::io_context io;
net::co_spawn(io, listen_accept(io), net::detached);
io.run();
}
co_spawn launches coroutines on an executor; co_await chains completion tokens without callback nesting.
8. std::generator (C++23)
#include <generator>
#include <ranges>
std::generator<int> fibonacci() {
int a = 0, b = 1;
while (true) {
co_yield a;
auto next = a + b;
a = b;
b = next;
}
}
for (int v : fibonacci() | std::views::take(10))
printf("%d\n", v);
Lazy sequences without manual coroutine handle management — compiler provides std::generator promise type.
9. Coroutine frame layout in GDB
g++ -std=c++20 -g -O0 -o app app.cpp
gdb ./app
(gdb) break my_coro
(gdb) run
(gdb) info frame # current stack frame
(gdb) info locals # promise, handle in scope
# Inspect coroutine frame pointer (compiler-specific mangling)
(gdb) p *(MyPromise*)h.address() # h = coroutine_handle
# GCC coroutine support (GCC 14+)
(gdb) info coroutines
# Pretty-print promise state
(gdb) set print pretty on
(gdb) p promise
Suspended coroutines may not appear on stack until resumed — trace via stored coroutine_handle.
10. Compilation time impact
Coroutines increase template instantiation and header parsing cost:
| Mitigation | Effect |
|---|
-O2 HALO | Reduces generated frame glue |
Out-of-line co_await in .cpp | Cuts recompilation cascade |
| Pimpl for coroutine return types | Hides awaitable templates from headers |
ccache / modules | See skills/rust/rust-build-times patterns for C++ |
Measure with g++ -ftime-report or clang -ftime-trace. Coroutine-heavy headers (Asio) benefit from unity builds sparingly — balance with RAM use.
11. Common pitfalls
| Issue | Cause | Fix |
|---|
co_await in a non-coroutine | Function missing coroutine return type | Change return type to a coroutine type |
Dangling handle after co_return | Using handle after coroutine finishes | Check handle.done() before resume |
| Double-resume | Resuming an already-resumed coroutine | Track state; only resume when suspended |
| Coroutine frame never freed | Forgot handle.destroy() | Use RAII wrapper (Task, Generator) |
| Heap allocation overhead | New frame per coroutine call | Enable HALO (Heap Allocation eLision Optimization) with -O2 |
| Recursive co_await depth | Stack overflow from deep chains | Use std::coroutine_handle<> tail-call pattern |
Related skills
- Use
skills/compilers/cpp-templates for other advanced C++20 features
- Use
skills/rust/rust-async-internals for Rust's equivalent Future/Poll model
- Use
skills/debuggers/gdb for GDB session management