Skip to content
C++
Language
since C++23
Intermediate

std::generator (C++23)

C++23 std::generator produces lazy, pull-based sequences via co_yield. Models input_range — compose with views, use recursive elements_of, yield by reference.

std::generator<Ref, V, Allocator>since C++23

A synchronous, pull-based coroutine type that produces a lazy input range by suspending at each co_yield expression and resuming on iterator increment.

Overview

std::generator is the first concrete coroutine type shipped in the C++ standard library, arriving in C++23 via <generator>. Unlike general coroutines built on <coroutine>, it provides a ready-made, range-compatible interface: a generator object models std::ranges::input_range and can be passed directly to any algorithm or view that accepts a range.

The coroutine body may use co_yield and co_return, but not co_awaitstd::generator is strictly synchronous and pull-based. Execution does not begin until the caller first advances the iterator (i.e., calls begin()). Each subsequent increment resumes the coroutine until the next co_yield or until the function returns. The generator is move-only and single-pass: iterating it twice, or copying it, is not possible.

The coroutine frame is heap-allocated by default, though compilers may apply Heap Allocation ELision Optimization (HALO) when the generator's lifetime is provably bounded within the caller's frame.

Syntax

cpp
// <generator>, C++23
template<
    class Ref,
    class V         = void,  // value_type; void → std::remove_cvref_t<Ref>
    class Allocator = void   // frame allocator; void → global new/delete
>
class std::generator;

The Ref template parameter is the reference type seen by the iterator's operator*. V is the value type — rarely specified explicitly. Common instantiations:

cpp
std::generator<int>              // value semantics: yields int&&, value_type = int
std::generator<int&>             // lvalue ref: yields int&, value_type = int
std::generator<const std::string&> // const ref: no copies on yield
std::generator<int, long>        // Ref=int, V=long — explicit value_type override

Yielding a nested range delegates to std::ranges::elements_of (C++23):

cpp
co_yield std::ranges::elements_of(some_range);
co_yield std::ranges::elements_of(some_range, alloc); // with allocator hint

Examples

Finite and infinite sequences

cpp
#include <generator>
#include <ranges>
#include <print>

std::generator<int> range(int first, int last) {  // C++23
    for (int i = first; i < last; ++i)
        co_yield i;
}

std::generator<long long> fibonacci() {           // infinite, C++23
    long long a = 0, b = 1;
    for (;;) {
        co_yield a;
        std::tie(a, b) = std::pair{b, a + b};
    }
}

// Both compose naturally with ranges views (C++20/23)
for (int i : range(0, 5))
    std::print("{} ", i);  // 0 1 2 3 4

auto fibs = fibonacci() | std::views::take(10);   // C++20
for (long long f : fibs)
    std::print("{} ", f);  // 0 1 1 2 3 5 8 13 21 34

Yielding by reference

Instantiate with a reference type to avoid copies when iterating containers:

cpp
#include <generator>
#include <vector>
#include <string>

// T& reference type — operator* returns string&, no copies
std::generator<const std::string&>
lines_of(const std::vector<std::string>& v) {
    for (const auto& s : v)
        co_yield s;
}

std::vector<std::string> data = {"alpha", "beta", "gamma"};
for (const std::string& s : lines_of(data))
    process(s);  // zero copies

Lifetime caveat: the yielded reference is only valid until the next iterator increment. Storing it past that point is undefined behaviour.

Recursive delegation with elements_of

Naive recursive generators that co_yield child generators one element at a time create O(depth) iterator chains per element. std::ranges::elements_of (C++23) solves this by delegating directly into the child coroutine frame:

cpp
#include <generator>

struct Node {
    int value;
    std::vector<Node> children;
};

std::generator<int> preorder(const Node& n) {
    co_yield n.value;
    for (const auto& child : n.children)
        co_yield std::ranges::elements_of(preorder(child));  // O(1) per element
}

Node tree{1, {{2, {{4, {}}, {5, {}}}}, {3, {{6, {}}}}}};
for (int v : preorder(tree))
    std::print("{} ", v);  // 1 2 4 5 3 6

Without elements_of, the same recursion degrades to O(n·depth) because each layer adds an iterator wrapper.

Composing with ranges algorithms

std::generator is an input range, so it participates in the full ranges pipeline:

cpp
#include <generator>
#include <ranges>
#include <algorithm>
#include <numeric>

std::generator<int> primes() {
    std::vector<int> known;
    for (int n = 2; ; ++n) {
        if (std::ranges::none_of(known, [n](int p){ return n % p == 0; })) {
            known.push_back(n);
            co_yield n;
        }
    }
}

// ranges::to (C++23) — collect into a container
auto first_20 = primes()
    | std::views::take(20)
    | std::ranges::to<std::vector>();  // C++23

// fold_left (C++23) — sum primes under 100
int total = std::ranges::fold_left(
    primes() | std::views::take_while([](int p){ return p < 100; }),
    0, std::plus{});

I/O streaming

cpp
#include <generator>
#include <fstream>
#include <string>

std::generator<std::string> read_lines(std::string_view path) {
    std::ifstream f{std::string(path)};
    std::string line;
    while (std::getline(f, line))
        co_yield line;
}

// Process a large file with backpressure — only reads what's consumed
for (const auto& line : read_lines("data.csv")
     | std::views::filter([](const std::string& l){ return !l.starts_with('#'); })
     | std::views::take(1000))
{
    ingest(line);
}

Best Practices

Prefer std::generator over hand-rolled iterators for stateful sequences. Custom sentinel-based iterators require a sentinel type, an iterator class, begin/end wiring, and correct copy/move semantics — generators express the same logic linearly, with the compiler handling the state machine.

Use elements_of for all recursive or delegating generators. Any pattern that yields from a sub-generator inside a loop must use co_yield std::ranges::elements_of(sub) rather than an inner for-loop, or performance degrades quadratically with depth.

Choose the right Ref type upfront. generator<T> copies or moves each yielded value; generator<T&> yields references. The latter is faster for large objects but requires the referent to outlive each iteration step. generator<const T&> is the common middle ground for read-only traversal.

Control frame allocation for hot paths. Pass a custom allocator as the third template argument, or use the std::allocator_arg_t tag in the coroutine parameter list to provide a per-call allocator. Stack allocators eliminate heap traffic entirely when generator lifetime is bounded.

Common Pitfalls

Calling begin() more than once. std::generator is a single-pass range. A second call to begin() after iteration has started is undefined behaviour. Store the iterator if you need to resume iteration manually.

cpp
auto gen = fibonacci();
auto it  = gen.begin();  // starts the coroutine
++it; ++it;
// auto it2 = gen.begin();  // UB — do not do this

Storing a yielded reference past the next increment. With generator<T&>, the reference returned by operator* is backed by a variable inside the coroutine frame. After ++it, that frame location may hold the next value:

cpp
auto& ref = *it;  // valid now
++it;
use(ref);         // UB — frame updated

Moving a generator during iteration. Moving a std::generator invalidates all iterators pointing into it. Do not move the generator object while holding a live iterator.

Expecting co_await to work. std::generator's promise type does not define await_transform, so co_await inside a generator body is a compile error. For asynchronous lazy sequences, you need a separate async generator type (e.g., from Asio or cppcoro).

Quadratic recursion without elements_of. Yielding child generator elements in a for-loop creates a chain of iterator layers:

cpp
// Slow — O(depth) chain per element:
for (int v : child_generator())
    co_yield v;

// Correct — O(1) per element:
co_yield std::ranges::elements_of(child_generator());

See Also