QUICK START:HTMLCSSJavaScriptTypeScriptPythonSQLReactNext.jsNode.jsLinux & UbuntuKotlinSwiftC# / .NETJavaGoRustC++DSASystem DesignDevOpsCybersecurityAI / ML
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

cpp
cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// C++20 Stackless Coroutines: Lazy Generator Architecture
#include <iostream>
#include <coroutine>
#include <optional>
template<typename T>
struct Generator {
// 1. Mandatory promise_type contract
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() noexcept { return {}; } // Lazy start
std::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-demand
Generator<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 Code

Common 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 value

Industry 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.