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

  1. What does co_await do?
  2. What part of the coroutine holds the return object and state?
Show answers
  1. Suspends the coroutine until the awaited operation completes, then resumes and returns a value.
  2. The promise_type associated with the coroutine frame holds state and constructs the return object.

Exercises

  1. Extend the generator to yield Fibonacci numbers lazily.
  2. Wrap a simple timer awaiter that resumes after a delay (requires platform scheduling).