C++ - Coroutines (co_await, co_yield)
Overview
Estimated time: 80–100 minutes
Write asynchronous and lazy computations with coroutines. Understand co_return, co_await, co_yield, and the roles of promise types and awaiters.
Learning Objectives
- Explain the coroutine transformation and roles: promise, handle, awaitable.
- Use co_await for async tasks and co_yield for generators.
- Recognize when coroutines simplify state machines.
Prerequisites
co_await with std::future (illustrative)
Note: Standard interop with std::future is limited. Many codebases use coroutine-aware libraries. Example below illustrates intent.
#include
#include
#include
// Pseudo-awaiter for std::future (illustrative only)
struct future_awaiter {
std::future& fut;
bool await_ready() const { return fut.wait_for(std::chrono::seconds(0)) == std::future_status::ready; }
void await_suspend(std::coroutine_handle<>) { /* real impl would schedule */ }
int await_resume() { return fut.get(); }
};
struct task {
struct promise_type {
task get_return_object() { return {}; }
std::suspend_never initial_suspend() noexcept { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
};
task run(){
auto fut = std::async(std::launch::async, []{ return 42; });
int x = co_await future_awaiter{fut};
std::cout << x << "\n";
}
int main(){ run(); }
Minimal generator (co_yield)
#include
#include
struct generator {
struct promise_type {
int current;
generator get_return_object(){ return generator{ std::coroutine_handle::from_promise(*this) }; }
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(int v){ current = v; return {}; }
void return_void() {}
void unhandled_exception(){ std::terminate(); }
};
std::coroutine_handle h;
generator(std::coroutine_handle h):h(h){}
generator(generator&& o):h(std::exchange(o.h,{})){}
~generator(){ if (h) h.destroy(); }
bool next(){ if (!h || h.done()) return false; h.resume(); return !h.done(); }
int value() const { return h.promise().current; }
};
generator count3(){ co_yield 1; co_yield 2; co_yield 3; }
int main(){
auto g = count3();
while (g.next()) std::cout << g.value() << " ";
}
Expected Output: 1 2 3
Common Pitfalls
- Building from scratch is verbose; prefer library abstractions or frameworks that provide coroutine-aware types.
- Beware lifetime of captured references across suspension points.
- Threading and coroutines are orthogonal; scheduling is library/runtime-specific.
Checks for Understanding
- What does co_await do?
- What part of the coroutine holds the return object and state?
Show answers
- Suspends the coroutine until the awaited operation completes, then resumes and returns a value.
- The promise_type associated with the coroutine frame holds state and constructs the return object.
Exercises
- Extend the generator to yield Fibonacci numbers lazily.
- Wrap a simple timer awaiter that resumes after a delay (requires platform scheduling).