Skip to content

🚀 C++ Coroutines — concise tutorial

What are coroutines?

Coroutines are functions that can suspend and later resume execution. They make asynchronous, lazy, or incremental control flow easier to write. C++ exposes coroutines via the keywords co_await, co_yield, and co_return, plus small library types such as std::suspend_always, std::suspend_never, and std::coroutine_handle.


Why use coroutines?

  • Write asynchronous code with straightforward control flow.
  • Build generators (lazy sequences) without manual state machines.
  • Integrate with event loops and async I/O more naturally than raw threads.

Basic usage (short)

  • co_yield value: suspend and produce a value (useful for generators).
  • co_await expr: await an awaitable (suspends according to the awaitable's behavior).
  • co_return value: finish the coroutine and optionally provide a result.

Coroutines require a coroutine-return type that defines a nested promise_type which the compiler uses to manage the coroutine frame and lifecycle.


Minimal generator example

This is a small single-file example that yields integers. Save as generator.cpp and compile with a recent C++20-capable compiler.

#include <coroutine>
#include <iostream>

struct Generator {
    struct promise_type {
        int value;
        std::suspend_always yield_value(int v) noexcept { value = v; return {}; }
        std::suspend_always initial_suspend() noexcept { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        Generator get_return_object() noexcept { return Generator{std::coroutine_handle<promise_type>::from_promise(*this)}; }
        void return_void() noexcept {}
        void unhandled_exception() { std::terminate(); }
    };

    using handle_t = std::coroutine_handle<promise_type>;
    explicit Generator(handle_t h) : h_(h) {}
    ~Generator() { if (h_) h_.destroy(); }

    bool next() { if (!h_) return false; h_.resume(); return !h_.done(); }
    int current() const { return h_.promise().value; }

private:
    handle_t h_;
};

Generator counter(int n) {
    for (int i = 0; i < n; ++i) co_yield i;
}

int main() {
    auto g = counter(5);
    while (g.next()) std::cout << "value: " << g.current() << '\n';
}

Key notes: - co_yield maps to promise_type::yield_value which stores the yielded value and suspends. - The caller resumes the coroutine via the handle; the example exposes next()/current() as a minimal API.


Simple awaiter example (illustrates co_await)

This toy awaiter suspends and immediately resumes the coroutine (demo only — a real scheduler would resume later).

#include <coroutine>
#include <iostream>

struct OneShot {
    bool await_ready() const noexcept { return false; }
    void await_suspend(std::coroutine_handle<> h) const noexcept { h.resume(); }
    void await_resume() const noexcept {}
};

struct Task {
    struct promise_type {
        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(); }
    };
};

Task demo() {
    std::cout << "before await\n";
    co_await OneShot{};
    std::cout << "after await\n";
}

int main() { demo(); }

Key notes: - await_ready() controls immediate continuation vs suspension. - await_suspend() receives the coroutine handle and may schedule resume later. - await_resume() returns an awaited value (if any).


How to compile / try

  1. Save an example as example.cpp (use one example per file).
  2. Compile with a modern compiler:
# clang
clang++ -std=c++20 example.cpp -O2 -o example

# g++ (recent versions)
g++ -std=c++20 example.cpp -O2 -o example

If you see errors mentioning <coroutine>, try: - a newer compiler, or - replacing #include <coroutine> with #include <experimental/coroutine> for older toolchains.


Common pitfalls & tips

  • Header differences: older toolchains use <experimental/coroutine>.
  • Coroutines do not automatically create threads — resumption is explicit and must be coordinated by your scheduler.
  • Take care when resuming coroutine handles across threads — protect shared state.
  • Keep coroutine bodies small for easier debugging.

Summary

  • Coroutines simplify async and lazy patterns in C++.
  • Start with simple generators and awaiters to learn the promise/awaiter model.
  • For production, consider established libraries (cppcoro) or integrate coroutines into your event loop.