[2026] C++20 Coroutines: co_await, co_yield, and Escaping Callback Hell

[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

MethodRole
get_return_objectReturn handle/wrapper to caller
initial_suspend / final_suspendControl suspend on entry/exit
yield_valueFor co_yield
return_value / return_voidFor co_return
unhandled_exceptionStore 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

  1. Dangling references to temporaries across suspend points—pass by value.
  2. Resuming destroyed handles.
  3. Conflicting return_value / return_void.
  4. Calling shared_from_this in constructor—use start() after make_shared.
  5. 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.

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3