C++20 Coroutines: From Syntax to Practical Use Cases

======================================================

Coroutines were long a feature of the C++ language that developers had to chase through workarounds and external libraries. With C++20, the standard finally provides first‑class support, giving us a clean syntax, well‑defined lifetimes, and a set of awaitable types that can be composed freely. In this article we’ll walk through the core concepts, show how to write a simple generator, explore std::generator, and discuss how coroutines can simplify asynchronous I/O, lazy evaluation, and stateful computations.

1. The Core Idea

A coroutine is a function that can suspend its execution and later resume from the same point. Think of it as a lightweight cooperative thread that can pause at designated points (co_await, co_yield, or co_return) and preserve its stack and local state. The compiler transforms the coroutine into a state machine under the hood; the programmer simply writes a natural, sequential style of code.

The primary language constructs introduced for coroutines are:

Keyword Purpose
co_await Suspend until an awaitable yields control.
co_yield Suspend and produce a value to the caller (generators).
co_return Finish the coroutine, optionally returning a value.

2. The Awaitable Interface

A type can be awaited if it satisfies the awaitable protocol. The standard defines this protocol in terms of three member functions:

bool await_ready();      // Is the operation ready immediately?
void await_suspend(std::coroutine_handle<>) ; // Called if not ready
T   await_resume();      // Result after resumption

The compiler calls these in the order:

  1. await_ready() – if true, the coroutine continues without suspension.
  2. await_suspend(handle) – may suspend the coroutine. It may also resume it immediately.
  3. await_resume() – obtains the result when the coroutine resumes.

3. A Minimal Coroutine: my_async_task

Below is a minimal awaitable that simulates an asynchronous operation using std::this_thread::sleep_for. It demonstrates how to wrap a blocking operation in a coroutine-friendly interface:

#include <coroutine>
#include <chrono>
#include <thread>
#include <iostream>

struct my_async_task {
    struct promise_type {
        my_async_task get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };
};

my_async_task async_sleep(std::chrono::milliseconds ms) {
    std::cout << "Sleeping for " << ms.count() << " ms\n";
    std::this_thread::sleep_for(ms);
    co_return;
}

Using this:

int main() {
    async_sleep(std::chrono::milliseconds(500));
}

Even though the coroutine never suspends, the example shows how the promise_type controls the coroutine’s lifecycle.

4. Generators with std::generator

C++20 introduced `std::generator

`, a standard awaitable that behaves like a lazy sequence. Under the hood, it implements the coroutine protocol with `co_yield`. Here’s a classic Fibonacci generator: “`cpp #include #include std::generator fib(int n) { int a = 0, b = 1; for (int i = 0; i < n; ++i) { co_yield a; std::tie(a, b) = std::make_pair(b, a + b); } } “` Consuming it: “`cpp int main() { for (int value : fib(10)) { std::cout << value << ' '; } std::cout << '\n'; } “` Output: “` 0 1 1 2 3 5 8 13 21 34 “` The generator lazily computes values on each iteration, making it memory efficient and ideal for streaming data. ### 5. Async I/O with `co_await` and `std::future` While `std::generator` handles synchronous iteration, asynchronous I/O typically uses `std::future` or custom awaitables. For example, with `std::future`, you can await a background computation: “`cpp #include #include int heavy_computation() { std::this_thread::sleep_for(std::chrono::seconds(2)); return 42; } std::future run_async() { return std::async(std::launch::async, heavy_computation); } int main() { auto fut = run_async(); std::cout << "Waiting for result…\n"; int result = fut.get(); // Blocks until ready std::cout << "Result: " << result << '\n'; } “` To make this coroutine-friendly, wrap the future in an awaitable: “`cpp struct future_awaiter { std::future & fut; bool await_ready() { return fut.wait_for(std::chrono::seconds(0)) == std::future_status::ready; } void await_suspend(std::coroutine_handle h) { std::thread([h, &fut]() { fut.wait(); h.resume(); }).detach(); } int await_resume() { return fut.get(); } }; future_awaiter co_await_future(std::future & fut) { return {fut}; } auto async_wrapper() -> std::generator { std::future fut = run_async(); int value = co_await co_await_future(fut); co_yield value; } “` Now the coroutine suspends until the async operation completes, resuming seamlessly. ### 6. Practical Use Cases | Scenario | Coroutine Benefit | Example | |———-|——————-|———| | **Lazy Streams** | No materialization of entire data set | `std::generator` for file lines, sensor data | | **Async I/O** | Non-blocking suspension, simpler flow | `co_await` with sockets or `std::future` | | **State Machines** | Encapsulate complex state transitions | Game AI behaviors, protocol handlers | | **Undo/Redo** | Store snapshots lazily | Co-routines that capture state on demand | | **Reactive Programming** | Combine streams easily | `co_yield` to produce UI events | ### 7. Pitfalls & Best Practices 1. **Avoid Blocking in Coroutines**: A coroutine that blocks the thread (e.g., `std::this_thread::sleep_for`) defeats the purpose of asynchrony. Use awaitables that yield control. 2. **Lifetime Management**: The coroutine’s promise object lives until the coroutine completes. Be careful with captures; use `std::move` for expensive resources. 3. **Exception Safety**: `unhandled_exception` in the promise should be defined. Prefer `std::terminate()` or propagate the exception. 4. **Stack Size**: Coroutines preserve local variables but not the call stack; however, deep recursion can still exhaust the stack if not careful. 5. **Deterministic Destruction**: Resources that need deterministic cleanup must be wrapped in a `std::unique_ptr` or a custom `finally` pattern inside the coroutine. ### 8. Conclusion C++20’s coroutine support opens a new paradigm for writing asynchronous, lazy, and stateful code. By turning the language itself into a cooperative concurrency primitive, developers can express complex flows in a clean, linear style. Whether you’re building a high‑performance network server, a lazy data pipeline, or a responsive UI, coroutines provide a powerful toolset that integrates seamlessly with the rest of the language. Dive in, experiment with generators and awaitables, and let the compiler do the heavy lifting while you keep the code readable.

发表评论