C++20 Coroutines: co_await, co_yield, and Escaping Callback Hell
이 글의 핵심
Learn coroutine keywords, how awaitables work, implement a minimal Generator and Task, avoid dangling references, and compare coroutines to callbacks and threads.
Introduction: “I want values one at a time” or “async without callback pyramids”
Coroutines suspend and resume at well-defined points. co_yield builds lazy generators; co_await waits for completion of awaitable work; co_return finishes with an optional value.
Coroutines are not OS threads: they are cooperative flows, often on one thread, unless you resume from a thread pool.
Build with g++ -std=c++20 or clang++ -std=c++20.
Callback hell vs coroutines
Nested async callbacks become unreadable; co_await keeps sequential structure and allows try/catch across steps.
Keywords
- co_yield expr — yield a value and suspend (generator).
- co_await expr — suspend until the awaitable completes.
- co_return — complete the coroutine (value or void).
Any function body using one of these becomes a coroutine; the compiler lowers it using promise_type on the return type.
Awaitable interface
Typical awaitable provides await_ready, await_suspend(handle), await_resume. If await_ready() is true, there is no suspend.
promise_type essentials
| Method | Role |
|---|---|
| get_return_object | Return handle/wrapper to caller |
| initial_suspend / final_suspend | Control suspend on entry/exit |
| yield_value | For co_yield |
| return_value / return_void | For co_return |
| unhandled_exception | Store or rethrow |
Do not define both return_value and return_void in one promise.
Lifetime
Suspended locals live in the coroutine frame on the heap. Keep Generator/Task objects alive while the frame is needed; avoid storing coroutine_handle past the owning object’s destruction.
Minimal Generator (conceptual)
The article includes a full Generator<int> with iterator, co_yield in a loop, and shared_ptr lifetime for buffers in async examples.
Task and exceptions
Store std::exception_ptr in unhandled_exception and rethrow in get() after draining the coroutine.
Common mistakes
- Dangling references to temporaries across suspend points—pass by value.
- Resuming destroyed handles.
- Conflicting return_value / return_void.
- Calling shared_from_this in constructor—use
start()aftermake_shared. - Concurrent resume on the same coroutine without synchronization.
Performance (overview)
Coroutines add frame allocation overhead; for massive tiny coroutines, profile. For I/O-bound work, the clarity usually wins.
Related posts
- Async coroutines #23-3
- Generator #23-2
- Asio + coroutines
Next: Generator implementation #23-2
Previous: Custom concepts #22-2