Skip to content

Coroutines (C++20) — co_await, co_yield, co_return, Generators, Awaitable Types

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about Coroutines (C++20). We cover key concepts, practical examples, and best practices to help you master this topic.

C++20 coroutines are stackless functions that can suspend execution and resume later, using co_await for asynchronous operations, co_yield for generator sequences, and co_return for returning values.

What You'll Learn

You will write generator coroutines with co_yield to produce lazy sequences, use co_await to suspend execution on asynchronous operations, understand the Coroutine frame, promise object, and awaitable type, implement simple awaiters and promise types, and recognize the tradeoffs of stackless coroutines vs threads.

Why It Matters

Coroutines enable async code that reads like synchronous code, eliminating callback chains. They are the foundation of C++20 asynchronous programming, used in networking libraries, game engines, and UI frameworks. C++ coroutines are zero-overhead when suspended and support complex control flow (loops, try/catch) that callback-based APIs cannot match.

Learning Path

graph LR
    A["56: Fold Expressions"] --> B["57: Coroutines"]
    B --> C["58: Modules"]
    C --> D["59: Exception Safety"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff

Generator Coroutine with co_yield

The simplest coroutine: a generator that produces a sequence of values lazily.

#include <iostream>
#include <generator>
#include <ranges>

// C++23 generator (simplified — real implementation needs a promise type)
// For C++20, we use a minimal generator:

template <typename T>
struct Generator {
    struct promise_type {
        T current_value_;
        bool finished_ = false;

        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        Generator get_return_object() {
            return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        void return_void() { finished_ = true; }
        void unhandled_exception() { std::terminate(); }

        std::suspend_always yield_value(T value) {
            current_value_ = std::move(value);
            return {};
        }
    };

    std::coroutine_handle<promise_type> handle_;

    explicit Generator(std::coroutine_handle<promise_type> h) : handle_(h) {}
    ~Generator() { if (handle_) handle_.destroy(); }

    // Move-only
    Generator(Generator&& other) noexcept : handle_(std::exchange(other.handle_, {})) {}
    Generator& operator=(Generator&& other) noexcept {
        if (this != &other) {
            if (handle_) handle_.destroy();
            handle_ = std::exchange(other.handle_, {});
        }
        return *this;
    }

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

    T value() const { return handle_.promise().current_value_; }

    // Range support
    struct End {};
    struct Iterator {
        Generator* gen_;
        bool operator!=(End) const { return gen_ && !gen_->handle_.done(); }
        void operator++() { gen_->next(); }
        T operator*() const { return gen_->value(); }
    };
    Iterator begin() {
        if (handle_) handle_.resume();
        return Iterator{this};
    }
    End end() { return {}; }
};

// Generator coroutine
Generator<int> fibonacci(int n) {
    int a = 0, b = 1;
    for (int i = 0; i < n; ++i) {
        co_yield a;  // Suspend, return 'a' to caller
        int next = a + b;
        a = b;
        b = next;
    }
}

int main() {
    // Use as a range
    for (int val : fibonacci(10)) {
        std::cout << val << " ";
    }
    std::cout << "\n";  // 0 1 1 2 3 5 8 13 21 34

    // Manual iteration
    auto fib = fibonacci(5);
    while (fib.next()) {
        std::cout << fib.value() << " ";
    }
    std::cout << "\n";  // 0 1 1 2 3
}

co_await — Suspending Execution

co_await suspends the current coroutine until the awaitable completes.

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

// Simple awaitable: suspend for a duration
struct SleepAwaitable {
    std::chrono::milliseconds duration_;

    // "await_ready": should we suspend or proceed immediately?
    bool await_ready() const noexcept { return duration_.count() == 0; }

    // "await_suspend": what happens when we suspend?
    void await_suspend(std::coroutine_handle<> handle) const {
        auto start = std::chrono::steady_clock::now();
        std::thread([handle, start, duration = duration_]() {
            std::this_thread::sleep_for(duration);
            // Resume the coroutine on the thread
            handle.resume();
        }).detach();
    }

    // "await_resume": what value does co_await return?
    void await_resume() const noexcept {}
};

// Simple async task
struct Task {
    struct promise_type {
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        Task get_return_object() {
            return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };
    std::coroutine_handle<promise_type> handle_;
    ~Task() { if (handle_) handle_.destroy(); }
};

Task asyncExample() {
    std::cout << "Starting async operation...\n";
    
    co_await SleepAwaitable{std::chrono::milliseconds(500)};
    
    std::cout << "After 500ms delay\n";
    
    co_await SleepAwaitable{std::chrono::milliseconds(300)};
    
    std::cout << "After another 300ms\n";
}

int main() {
    std::cout << "Before coroutine\n";
    asyncExample();
    std::cout << "After coroutine (might print before coroutine completes)\n";

    // Wait for detached threads to finish
    std::this_thread::sleep_for(std::chrono::seconds(2));
}

Awaitable Traits

Types used with co_await must implement three methods or specialize std::await_ready, std::await_suspend, std::await_resume.

#include <iostream>
#include <coroutine>

// Manual awaitable with value return
struct IntProvider {
    int value_;

    bool await_ready() const noexcept { return false; }

    // Return value controls suspension behavior:
    // - void: unconditionally suspend
    // - bool: true = suspend, false = don't suspend
    // - coroutine_handle: resume a different coroutine
    void await_suspend(std::coroutine_handle<> handle) {
        std::cout << "Suspending coroutine\n";
        // In real code: store handle, resume later
    }

    int await_resume() const noexcept {
        return value_;
    }
};

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

IntTask testAwait() {
    // co_await expression evaluates to await_resume() return value
    int result = co_await IntProvider{42};
    std::cout << "Got: " << result << "\n";  // 42
}

int main() {
    testAwait();
}

Coroutine Frame and Lifetime

When a function body contains co_await, co_yield, or co_return, it becomes a coroutine. The compiler allocates a coroutine frame (heap-allocated by default) to hold the suspended state.

#include <iostream>
#include <coroutine>

// Demonstrate frame lifetime
struct LifecycleTask {
    struct promise_type {
        int id_;
        
        promise_type() : id_(counter_++) {
            std::cout << "Promise created (id=" << id_ << ")\n";
        }
        ~promise_type() {
            std::cout << "Promise destroyed (id=" << id_ << ")\n";
        }

        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        LifecycleTask get_return_object() {
            return LifecycleTask{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
        static int counter_;
    };
    std::coroutine_handle<promise_type> handle_;
    ~LifecycleTask() {
        if (handle_) {
            std::cout << "Destroying task\n";
            handle_.destroy();
        }
    }
};
int LifecycleTask::promise_type::counter_ = 0;

LifecycleTask lifecycleExample() {
    std::cout << "Inside coroutine\n";
    co_await std::suspend_always{};
    std::cout << "After first suspend\n";
    co_await std::suspend_always{};
    std::cout << "After second suspend\n";
}

int main() {
    auto task = lifecycleExample();
    std::cout << "Back in main (coroutine suspended)\n";

    task.handle_.resume();  // Resume first time
    std::cout << "Back after first resume\n";

    task.handle_.resume();  // Resume second time
    std::cout << "Back after second resume\n";
}

Standard Awaitables: std::suspend_always and std::suspend_never

#include <iostream>
#include <coroutine>

// std::suspend_always: always suspends
//   await_ready() → false
//   await_suspend() → (suspends)
//   await_resume() → void

// std::suspend_never: never suspends
//   await_ready() → true
//   await_suspend() → (never called)
//   await_resume() → void

struct DemoTask {
    struct promise_type {
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        DemoTask get_return_object() {
            return {std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };
    std::coroutine_handle<promise_type> handle_;
    ~DemoTask() { if (handle_) handle_.destroy(); }
};

DemoTask demo() {
    std::cout << "Inside coroutine\n";
    co_await std::suspend_never{};   // Does NOT suspend
    std::cout << "Still inside (no suspend)\n";
    co_await std::suspend_always{};  // Does suspend
    std::cout << "After suspend\n";
}

int main() {
    auto t = demo();
    std::cout << "Main\n";
    t.handle_.resume();
    std::cout << "Main after resume\n";
}

Real-World: Generator with std::generator (C++23)

C++23 introduces std::generator<T> in <generator>.

#include <iostream>
#include <generator>
#include <ranges>

// C++23 std::generator (compile if available)
std::generator<int> range(int start, int end) {
    for (int i = start; i < end; ++i) {
        co_yield i;
    }
}

int main() {
    for (int x : range(0, 10) | std::views::filter([](int n) { return n % 2 == 0; })) {
        std::cout << x << " ";
    }
    std::cout << "\n";  // 0 2 4 6 8
}

Common Mistakes

Mistake 1: Forgetting final_suspend

Without final_suspend, the coroutine frame may be destroyed before you can read the result.

Mistake 2: Not destroying the coroutine handle

auto h = coro();  // Gets a handle
// Forgets to call h.destroy() — memory leak!

Coroutine frames must be explicitly destroyed or stored in a RAII wrapper.

Mistake 3: Thinking coroutines are threads

Coroutines are not threads. They are suspendable functions that run on a single thread unless you explicitly schedule them on another.

Mistake 4: Using coroutines in hot loops with heap allocation

Allocating the coroutine frame on every iteration is expensive. Use std::generator or pool allocators for performance.

Mistake 5: Ignoring the promise_type contract

The promise type must implement initial_suspend, final_suspend, get_return_object, return_void or return_value, and unhandled_exception. Missing any causes compilation errors.

Practice Questions

  1. What is a coroutine frame? Answer: A heap-allocated object that stores the suspended state of a coroutine, including local variables, the promise object, and resume point.

  2. What does co_yield do? Answer: It suspends the coroutine and returns a value to the caller. The coroutine can be resumed later to produce the next value.

  3. What three methods must an awaitable implement? Answer: await_ready, await_suspend, and await_resume.

  4. What is the difference between std::suspend_always and std::suspend_never? Answer: suspend_always always suspends. suspend_never never suspends. Use suspend_never for initial suspend when you want the coroutine to start immediately.

  5. Can a coroutine have both co_yield and co_return? Answer: Yes. co_return ends the coroutine. After co_return, co_yield is no longer valid.

FAQ

What are C++20 coroutines

Coroutines are functions that can suspend execution (via co_await, co_yield, co_return) and resume later. They are stackless — only the local state is saved.

How are coroutines different from threads

Coroutines are not threads. A coroutine suspends without blocking the thread, allowing other code to run on the same thread. Threads run concurrently; coroutines run cooperatively.

What is a promise_type in a coroutine

The promise_type defines the coroutine's behavior: how values are yielded, returned, and how errors are handled. It controls the coroutine's interaction with the caller.

Do coroutines allocate memory

Yes. Each coroutine allocates a coroutine frame on the heap (by default). Custom allocators can be provided via the promise_type.

Can I use coroutines with the STL

C++23 adds std::generator. For C++20, you need to write custom promise types or use libraries like cppcoro, boost.asio, or folly.

Mini Project

Build a coroutine-based Sequence generator that can produce arithmetic, geometric, and custom sequences:

#include <iostream>
#include <coroutine>

// Your Sequence<T> coroutine type

Sequence<int> arithmetic(int start, int step, int count) {
    for (int i = 0; i < count; ++i) {
        co_yield start + i * step;
    }
}

Sequence<int> geometric(int start, int ratio, int count) {
    int value = start;
    for (int i = 0; i < count; ++i) {
        co_yield value;
        value *= ratio;
    }
}

int main() {
    // Arithmetic: 0, 5, 10, 15, 20
    for (int val : arithmetic(0, 5, 5)) {
        std::cout << val << " ";
    }
    std::cout << "\n";

    // Geometric: 1, 2, 4, 8, 16
    for (int val : geometric(1, 2, 5)) {
        std::cout << val << " ";
    }
    std::cout << "\n";
}

This project demonstrates how C++ coroutines enable lazy, composable sequences — similar to generators in Python but with zero-allocation per iteration.

What's Next

You now understand coroutines — C++20's most powerful control flow feature. Next, you will learn modules (C++20), which replace header files with a modern, faster, and more hygienic compilation model.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro