Advanced 26 min readModule: Module 12: C++20 Coroutines: Promises, Awaitables & Symmetric Transfer
C++20 Coroutines Architecture: Promises & Awaitables
Build asynchronous generators and task schedulers with C++20 stackless coroutines: constructing `promise_type`, designing custom awaitables with `await_ready`, `await_suspend`, `await_resume`, and preventing stack overflow via Symmetric Transfer.
What You Will Learn in This Lesson
- Stackful coroutines (Fibers) vs C++20 Stackless Coroutines (Heap frame allocation)
- The 3 coroutine keywords: `co_await`, `co_yield`, and `co_return`
- The anatomy of `promise_type`: `get_return_object()`, `initial_suspend()`, `final_suspend()`
- Eliminating recursive stack overflow using Symmetric Transfer (`std::coroutine_handle<>`)
Introduction & Core Concept
C++20 introduces native stackless coroutines: functions that can suspend execution ('co_await', 'co_yield') and resume at a later time without blocking the calling thread. Unlike languages that provide a rigid, opaque async runtime (like JavaScript or C#), C++20 coroutines are completely customizable: developers define their own memory allocation, suspension hooks, and scheduling promises.
WHY DOES THIS MATTER IN THE REAL WORLD?
High-frequency trading order routers, game engines, and low-latency network engines (like Seastar) use C++20 coroutines to write asynchronous non-blocking event code that compiles to the exact same assembly as handwritten state machines.
Syntax & Structure
cpp
struct Task { struct promise_type { ... }; std::coroutine_handle<promise_type> handle;};Task async_fetch() { co_await std::suspend_always{}; }Implementing a Lazy Infinite Sequence Generator with C++20 Coroutines
cppcpp
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061// C++20 Stackless Coroutines: Lazy Generator Architecture#include <iostream>#include <coroutine>#include <optional>template<typename T>struct Generator {// 1. Mandatory promise_type contractstruct promise_type {T current_value;Generator get_return_object() {return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};}std::suspend_always initial_suspend() noexcept { return {}; } // Lazy startstd::suspend_always final_suspend() noexcept { return {}; }std::suspend_always yield_value(T value) noexcept {current_value = value;return {}; // Suspend and return value to caller}void return_void() noexcept {}void unhandled_exception() { std::terminate(); }};std::coroutine_handle<promise_type> handle;explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {}~Generator() { if (handle) handle.destroy(); }bool next() {if (!handle || handle.done()) return false;handle.resume();return !handle.done();}T value() const { return handle.promise().current_value; }};// 2. Coroutine Function emitting Fibonacci numbers on-demandGenerator<uint64_t> fibonacci_sequence() {uint64_t a = 0, b = 1;while (true) {co_yield a; // Suspends coroutine and returns 'a'uint64_t next = a + b;a = b;b = next;}}int main() {std::cout << "=== C++20 Coroutine Fibonacci Generator ===" << std::endl;auto fib = fibonacci_sequence();for (int i = 0; i < 8; ++i) {if (fib.next()) {std::cout << "Fibonacci #" << i << ": " << fib.value() << std::endl;}}return 0;}
Line-by-Line Technical Breakdown
1Symmetric Transfer: When resuming one coroutine from another, standard calls build call-stack frames that can cause stack overflow in recursive loops. In C++20, `await_suspend` can return `std::coroutine_handle<>`, which the compiler turns into a zero-stack-growth tail-jump to the next coroutine.
Try It Yourself (Interactive Editor)
Modify the code in real-time and click Run to test live browser output and console logs.
Intelligent Code Runner & Live Sandbox[CPP]
CPP SOURCE EDITOR
Interactive Live CodeCommon Mistakes & How to Avoid Them
#1: Passing parameters by reference (`const std::string&`) into coroutines that outlive the caller's scope.
When a coroutine suspends, the caller's stack frame may be destroyed. References become dangling pointers. Always pass arguments by value into coroutines.
Incorrect / Antipattern
Task async_task(const std::string& str) { co_await wait(); use(str); } // Dangling reference bug!Correct / Professional Solution
Task async_task(std::string str) { co_await wait(); use(str); } // Pass by valueIndustry Best Practices & Professional Standards
- Pass coroutine arguments by value to avoid dangling reference crashes.
- Use Symmetric Transfer (`await_suspend` returning `coroutine_handle<>`) to prevent recursive stack overflows.
- Overload `operator new` on `promise_type` to use custom arena allocators for coroutine frames.
Lesson Summary & Core Takeaways
- C++20 stackless coroutines compile down to zero-overhead state machines.
- `promise_type` controls creation, lifecycle, suspension, and return values.
- Symmetric Transfer provides tail-recursive coroutine switching with zero stack growth.