Skip to content
C++

Generators with co_yield (C++20/23)

A generator is a coroutine that produces a lazy sequence of values — one per call, on demand. Where a normal function computes everything and returns it all at once, a generator suspends at each co_yield statement, hands one value to the caller, and waits until the caller asks for the next. This means an infinite sequence (all Fibonacci numbers, all integers starting from zero) takes no more memory than a single element. C++20 provides the coroutine machinery to build generator types; C++23 ships std::generator<T> in <generator> as the standard library's first-class generator type.

How co_yield works

The expression co_yield v is syntactic sugar for co_await promise.yield_value(v). The promise's yield_value() implementation stores or forwards the value and returns an awaitable — almost always std::suspend_always, which immediately suspends the coroutine. Execution transfers back to whoever last called resume() on the coroutine handle. The yielded value lives in the promise object until the caller reads it. The next call to resume() continues the coroutine body from the line after the co_yield.

// A generator coroutine — each co_yield suspends and hands one value to the caller
generator<int> count_up(int start, int end) {
    for (int i = start; i < end; ++i) {
        co_yield i;   // suspends here, value i is available to the caller
        // execution resumes here on the next call to the iterator's operator++
    }
    // falling off the end completes the coroutine (calls return_void() on the promise)
}

// Consumed by a plain range-based for loop:
for (int n : count_up(0, 5)) {
    std::print("{} ", n);   // 0 1 2 3 4
}

Why generators need a different promise type than tasks

A task<T> returns a single value via co_return and the promise stores a return_value() member. A generator<T> returns many values via co_yield, so the promise needs a yield_value() member instead. Generators are also iterable, so the generator type must provide begin() and end() and an inner iterator class whose operator++ calls resume(). Unlike a task, a generator does not expose operator co_await — you do not await a generator, you iterate it.

Aspecttask<T>generator<T>
Coroutine keywordco_return vco_yield v (repeatedly)
Promise key memberreturn_value(v)yield_value(v)
Values producedOneMany (lazily)
Consumed viaco_await or execute() loopRange-based for loop
Exposesoperator co_awaitbegin() / end() / iterator

Building a generator<T> type

The generator type needs four pieces: a promise_type inner struct (mandatory name), an iterator inner struct, the generator's lifecycle members (constructor, destructor, move semantics), and begin()/end(). The promise stores a pointer to the most recently yielded value — a pointer rather than a copy because the value lives in the coroutine frame, and the iterator reads it directly.

The promise_type

template <typename T>
struct generator {
    struct promise_type {
        T const* value_ = nullptr;           // pointer into the frame
        std::exception_ptr eptr_ = nullptr;  // any uncaught exception

        auto get_return_object() { return generator{ *this }; }

        // Lazy: don't start until begin() calls resume()
        auto initial_suspend() noexcept { return std::suspend_always{}; }
        auto final_suspend()   noexcept { return std::suspend_always{}; }

        // Store the exception so iterating can rethrow it
        void unhandled_exception() noexcept {
            eptr_ = std::current_exception();
        }
        void rethrow_if_exception() {
            if (eptr_) std::rethrow_exception(eptr_);
        }

        // co_yield v calls yield_value(v), stores a pointer, then suspends
        auto yield_value(T const& v) {
            value_ = std::addressof(v);
            return std::suspend_always{};
        }

        void return_void() {}   // coroutine ends by falling off the end
    };
    // ... iterator and members below
};

The iterator (drives resumption)

    struct iterator {
        using iterator_category = std::input_iterator_tag;
        using value_type        = T;
        using reference         = T const&;
        using pointer           = T const*;
        using difference_type   = ptrdiff_t;

        std::coroutine_handle<promise_type> handle_ = nullptr;

        // Advance: resume until the next co_yield or completion
        iterator& operator++() {
            handle_.resume();
            if (handle_.done())
                std::exchange(handle_, {}).promise().rethrow_if_exception();
            return *this;
        }
        void operator++(int) { ++*this; }

        bool operator==(iterator const& rhs) const {
            return handle_ == rhs.handle_;
        }
        bool operator!=(iterator const& rhs) const { return !(*this == rhs); }

        reference operator*()  const { return *handle_.promise().value_; }
        pointer   operator->() const { return  handle_.promise().value_; }
    };

The generator class members

    // Created by get_return_object() via the promise
    explicit generator(promise_type& p)
        : handle_(std::coroutine_handle<promise_type>::from_promise(p)) {}

    generator() = default;
    generator(generator const&) = delete;
    generator& operator=(generator const&) = delete;

    generator(generator&& other) : handle_(other.handle_) {
        other.handle_ = nullptr;
    }
    generator& operator=(generator&& other) {
        if (this != std::addressof(other)) {
            handle_ = other.handle_;
            other.handle_ = nullptr;
        }
        return *this;
    }
    ~generator() { if (handle_) handle_.destroy(); }

    // begin() starts the coroutine running until the first co_yield
    iterator begin() {
        if (handle_) {
            handle_.resume();
            if (handle_.done()) {
                handle_.promise().rethrow_if_exception();
                return { nullptr };
            }
        }
        return { handle_ };
    }
    iterator end() { return { nullptr }; }

private:
    std::coroutine_handle<promise_type> handle_ = nullptr;

Practical generator examples

With the generator<T> type in place, writing lazy sequences becomes straightforward. An infinite generator simply loops forever — the caller controls how many values to consume by breaking out of the loop early.

// Infinite integer sequence starting at 'start', stepping by 'step'
generator<int> iota(int start = 0, int step = 1) noexcept {
    auto value = start;
    for (;;) {
        co_yield value;
        value += step;
    }
}

// Consume first 10 values:
for (auto i : iota()) {
    std::print("{} ", i);
    if (i >= 9) break;   // 0 1 2 3 4 5 6 7 8 9
}

// Finite sequence: yield n values then end naturally
generator<std::optional<int>> iota_n(int start = 0, int step = 1,
                                     int n = std::numeric_limits<int>::max()) noexcept {
    auto value = start;
    for (int i = 0; i < n; ++i) {
        co_yield value;
        value += step;
    }
}

// Fibonacci — an infinite lazy sequence, zero allocation beyond the frame
generator<int> fibonacci() noexcept {
    int a = 0, b = 1;
    while (true) {
        co_yield b;
        auto tmp = a;
        a = b;
        b += tmp;
    }
}

int c = 1;
for (auto i : fibonacci()) {
    std::print("{} ", i);    // 1 1 2 3 5 8 13 21 34 55
    if (++c > 10) break;
}

// Multiple co_yield statements without a loop
generator<int> get_values() noexcept {
    co_yield 1;
    co_yield 2;
    co_yield 3;
}

for (auto i : get_values())
    std::print("{} ", i);   // 1 2 3

std::generator<T> — the C++23 standard library type

C++23 adds std::generator<T> in header <generator>. It is the standard library's first-class coroutine type and obsoletes the hand-rolled version above for most purposes. It supports all ranges algorithms because it models std::ranges::input_range, and it handles recursive generators correctly. The function signature is identical to what you'd write with a hand-rolled type.

#include <generator>   // C++23
#include <print>
#include <chrono>

// std::generator<T> replaces hand-rolled generator<T> exactly
std::generator<int> get_sequence(int start_value, int count)
{
    using namespace std::chrono;
    for (int i = start_value; i < start_value + count; ++i) {
        // Timestamp each yield so the caller can see real elapsed time
        auto now = current_zone()->to_local(system_clock::now());
        std::print("{:%H:%M:%OS}: ", now);
        co_yield i;
    }
}

int main()
{
    auto gen = get_sequence(10, 5);
    for (const auto& value : gen) {
        std::print("{} (press enter for next value)", value);
        std::cin.ignore();
    }
    // Typical output:
    // 14:23:01: 10 (press enter for next value)
    // 14:23:02: 11 (press enter for next value)
    // ...
}
// std::generator composes with ranges algorithms
#include <generator>
#include <ranges>
#include <algorithm>

std::generator<int> naturals() {
    for (int i = 0;; ++i) co_yield i;
}

// Take first 10 even numbers: ranges pipeline over a generator
auto evens = naturals()
    | std::views::filter([](int n){ return n % 2 == 0; })
    | std::views::take(10);

for (int n : evens)
    std::print("{} ", n);   // 0 2 4 6 8 10 12 14 16 18

Alternatives: cppcoro and concurrencpp

If you are on C++20 without C++23 library support, the cppcoro library provides cppcoro::generator<T> (for synchronous sequences), cppcoro::async_generator<T> (for sequences that can co_await inside), and cppcoro::recursive_generator<T> (for efficiently yielding the elements of a nested sequence as elements of an outer sequence). The interface is a drop-in replacement: replace generator<T> with cppcoro::generator<T> and the coroutine body is unchanged.

#include <cppcoro/generator.hpp>

cppcoro::generator<int> fibonacci() noexcept {
    int a = 0, b = 1;
    while (true) {
        co_yield b;
        auto tmp = a;
        a = b;
        b += tmp;
    }
}

// cppcoro::async_generator — can co_await inside the body
cppcoro::async_generator<std::string> read_lines(std::string path) {
    auto file = co_await open_file_async(path);   // suspends until file is open
    std::string line;
    while (co_await file.read_line(line))          // suspends until line is ready
        co_yield line;
}

Limitations to know

co_await and co_yield cannot mix in a basic generator

The hand-rolled generator<T> above supports only co_yield — it does not allow co_await inside the body. Use cppcoro::async_generator<T> or build an async-aware promise type to mix both.

Generators are move-only

The coroutine frame is unique: copying a generator would require duplicating heap-allocated state. Always move generators, never copy them.

The iterator is an input iterator

You can only advance forward, one element at a time. There is no random access and no rewinding. For a reproducible sequence, create a new generator.

std::generator requires C++23

Compiler support: GCC 14+, Clang 18+, MSVC 19.37+ (Visual Studio 2022 17.7). Check your toolchain before relying on <generator>.

← Promise types and the coroutine frame
Async coroutines with co_await coming soon
Sign in to track progress