Exploring the New Features of C++20 Coroutines

Coroutines are one of the most exciting additions to the C++20 language, providing a powerful, lightweight abstraction for asynchronous programming, lazy evaluation, and cooperative multitasking. Unlike traditional threads, coroutines are stateful functions that can suspend and resume execution without the overhead of context switching or stack allocation. In this article, we’ll dive into the core concepts, explore practical usage patterns, and illustrate how coroutines can simplify complex control flows in modern C++.

1. Basic Coroutine Syntax

A coroutine is defined using the co_ keywords. The most common ones are:

  • co_yield: Produces a value and suspends until the next co_await or co_yield.
  • co_return: Terminates the coroutine, optionally returning a value.
  • co_await: Suspends until the awaited expression is ready.

Here’s a minimal example that generates an infinite sequence of Fibonacci numbers:

#include <coroutine>
#include <iostream>

struct fib_awaiter {
    unsigned long long a = 0, b = 1;
    bool await_ready() noexcept { return false; }
    void await_suspend(std::coroutine_handle<> h) noexcept { h.resume(); }
    unsigned long long await_resume() noexcept { 
        unsigned long long next = a + b; 
        a = b; b = next; 
        return a; 
    }
};

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

    auto operator()() const {
        for (;;) co_yield fib_awaiter{};
    }
};

int main() {
    auto gen = fib_generator{};
    auto cor = gen();
    for (int i = 0; i < 10; ++i) {
        std::cout << cor() << ' ';
    }
    std::cout << std::endl;
}

2. Coroutines vs. Threads

Feature Coroutines Threads
Memory overhead One stack frame per coroutine Full stack per thread
Scheduling Manual, cooperative Preemptive, OS
Synchronization None needed for cooperative Locks, atomic ops
Use cases IO, pipelines, generators Parallel computation

Coroutines excel at cooperative multitasking—each coroutine explicitly yields control. This eliminates the need for locks in many scenarios, simplifying code that would otherwise require careful synchronization.

3. Implementing a Lazy Sequence

Consider a large file that you need to process line by line. With coroutines, you can create a lazy iterator that reads lines on demand:

#include <coroutine>
#include <fstream>
#include <string>

struct line_reader {
    std::ifstream file;
    struct promise_type {
        std::string line;
        line_reader get_return_object() { return {}; }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
        void yield_value(const std::string& l) { line = l; }
    };

    std::coroutine_handle <promise_type> h;
    explicit line_reader(std::coroutine_handle <promise_type> h_) : h(h_) {}

    bool next() {
        if (!h.done()) {
            h.resume();
            return !h.done();
        }
        return false;
    }

    const std::string& current() const { return h.promise().line; }
};

line_reader read_lines(const std::string& filename) {
    std::ifstream f(filename);
    std::string line;
    while (std::getline(f, line))
        co_yield line;
}

int main() {
    auto lr = read_lines("bigfile.txt");
    while (lr.next())
        std::cout << lr.current() << '\n';
}

This approach avoids loading the entire file into memory, making it ideal for huge datasets.

4. Integrating with Asynchronous I/O

When combined with the standard library’s std::future and std::promise, coroutines can elegantly express asynchronous operations:

#include <future>
#include <chrono>
#include <iostream>

struct async_wait {
    std::future <void> fut;
    bool await_ready() noexcept { return fut.wait_for(std::chrono::seconds(0)) == std::future_status::ready; }
    void await_suspend(std::coroutine_handle<> h) { 
        std::thread([h] { h.resume(); }).detach(); 
    }
    void await_resume() noexcept {}
};

auto async_task() {
    std::cout << "Start async task\n";
    co_await async_wait{std::async(std::launch::async, []{ std::this_thread::sleep_for(std::chrono::seconds(2)); })};
    std::cout << "Async task finished\n";
}

Here, async_wait suspends until the asynchronous operation completes, resuming the coroutine automatically.

5. Common Pitfalls and Best Practices

Pitfall Remedy
Forgetting to co_await on a std::future Use co_await std::async(...) or wrap the future in a custom awaitable
Mismanaging coroutine handles Prefer `std::optional
` or RAII wrappers
Excessive state in the promise Keep the promise minimal; large state should live in the coroutine object itself
Deadlocks in cooperative code Ensure each coroutine yields frequently; avoid long-running computations without yielding

6. Future Directions

The C++ community continues to refine coroutine support. Upcoming proposals aim to integrate coroutine frames directly into the language’s type system, simplifying custom awaitables and reducing boilerplate. Meanwhile, libraries like cppcoro, cppcoro::generator, and Boost’s asio::awaitable are expanding the ecosystem, making coroutine-based programming increasingly accessible.


Coroutines represent a paradigm shift for C++ developers: they empower concise, readable, and efficient asynchronous code. By mastering co_yield, co_return, and co_await, you can rewrite legacy callback chains into elegant, linear flows that are both easier to reason about and maintain. Whether you’re building high‑performance servers, complex data pipelines, or simply want to write cleaner code, C++20 coroutines are a tool worth adding to your repertoire.

发表评论