--- 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 ```cpp // co_return — return a value and end the coroutine co_return value; // co_yield — produce a value, suspend, resume later co_yield value; // co_await — suspend until an awaitable completes 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 ```cpp #include #include #include template struct Task { struct promise_type { std::optional value; std::exception_ptr exception; Task get_return_object() { return Task{std::coroutine_handle::from_promise(*this)}; } std::suspend_always initial_suspend() { return {}; } // lazy start 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 handle; explicit Task(std::coroutine_handle h) : handle(h) {} Task(Task&&) = default; Task& operator=(Task&&) = default; ~Task() { if (handle) handle.destroy(); } T get() { handle.resume(); // resume to completion if (handle.promise().exception) std::rethrow_exception(handle.promise().exception); return std::move(*handle.promise().value); } }; // Usage Task compute() { co_return 42; } int main() { auto task = compute(); int result = task.get(); // 42 } ``` ### 3. Generator with co_yield ```cpp template struct Generator { struct promise_type { T current_value; Generator get_return_object() { return Generator{std::coroutine_handle::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 {}; // suspend after yielding } }; std::coroutine_handle handle; explicit Generator(std::coroutine_handle h) : handle(h) {} ~Generator() { if (handle) handle.destroy(); } struct iterator { std::coroutine_handle handle; bool done; iterator& operator++() { handle.resume(); done = handle.done(); return *this; } T operator*() const { return handle.promise().current_value; } bool operator!=(std::default_sentinel_t) const { return !done; } }; iterator begin() { handle.resume(); // advance to first yield return {handle, handle.done()}; } std::default_sentinel_t end() { return {}; } }; // Usage Generator iota(int start, int end) { for (int i = start; i < end; ++i) co_yield i; } for (int x : iota(0, 5)) { std::cout << x << ' '; // 0 1 2 3 4 } ``` ### 4. Awaitable — custom co_await target ```cpp // An awaitable has three methods: // await_ready() — true means don't suspend // await_suspend(handle) — suspend: store handle, schedule resume // await_resume() — return value of co_await expression struct TimerAwaitable { int delay_ms; bool await_ready() const noexcept { return delay_ms <= 0; } void await_suspend(std::coroutine_handle<> h) { // Schedule h.resume() to be called after delay std::thread([h, this]() { std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms)); h.resume(); }).detach(); } void await_resume() const noexcept {} // no return value }; // suspend_always and suspend_never are built-in awaitables std::suspend_always{}; // always suspends std::suspend_never{}; // never suspends (no-op) ``` ### 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 ```cpp // Inspect frame size with Compiler Explorer (godbolt.org) // Compile with: g++ -std=c++20 -O2 -S // Look for: operator new call size in the generated asm // Or: clang -std=c++20 -O2 -emit-llvm -S | grep "coro.size" // Reduce frame size: // 1. Don't keep large objects alive across co_await struct Bad { std::vector large_buf; // whole vector lives in frame co_return large_buf.size(); // large_buf crosses suspension }; // 2. Move data out before suspending std::vector buf = get_data(); size_t sz = buf.size(); // capture only what's needed buf.clear(); // release before suspension co_await next_event; // sz still valid; buf released ``` ### 6. Debugging suspended coroutines in GDB ```bash # Coroutines appear as regular stack frames after resume() # To inspect a suspended coroutine: (gdb) info locals # Look for coroutine_handle variables # Print the promise object (gdb) p *(promise_type*)(handle.__handle_) # GDB 14+ has coroutine-specific support (gdb) info coroutines # GCC coroutine support (experimental) # Step through coroutine execution (gdb) step # enters co_await implementation (gdb) finish # returns from coroutine frame function (gdb) next # step over suspension point # View all threads (coroutines running on thread pool) (gdb) info threads (gdb) thread 2 (gdb) bt ``` ### 7. Boost.Asio `co_spawn` and `co_await` ```cpp #include #include #include namespace net = boost::asio; net::awaitable 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) ```cpp #include #include std::generator fibonacci() { int a = 0, b = 1; while (true) { co_yield a; auto next = a + b; a = b; b = next; } } // Usage 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 ```bash # Compile with debug info g++ -std=c++20 -g -O0 -o app app.cpp gdb ./app ``` ```gdb (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