[2026] C++20 Coroutines: co_await, co_yield, and Escaping Callback Hell
이 글의 핵심
C++20 coroutines explained: co_yield generators, co_await async patterns, promise_type and coroutine_handle, lifetime pitfalls, Task/Generator sketches, and production notes with Asio.
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 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() after make_shared.
- Concurrent resume on the same coroutine without synchronization.