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

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

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

  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.


  • Async coroutines #23-3
  • Generator #23-2
  • Asio + coroutines

Next: Generator implementation #23-2

Previous: Custom concepts #22-2