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++23A 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_await — std::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
// <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:
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 overrideYielding a nested range delegates to std::ranges::elements_of (C++23):
co_yield std::ranges::elements_of(some_range);
co_yield std::ranges::elements_of(some_range, alloc); // with allocator hintExamples
Finite and infinite sequences
#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 34Yielding by reference
Instantiate with a reference type to avoid copies when iterating containers:
#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 copiesLifetime 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:
#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 6Without 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:
#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
#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.
auto gen = fibonacci();
auto it = gen.begin(); // starts the coroutine
++it; ++it;
// auto it2 = gen.begin(); // UB — do not do thisStoring 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:
auto& ref = *it; // valid now
++it;
use(ref); // UB — frame updatedMoving 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:
// 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
std::ranges::elements_of— C++23 delegation wrapper used withco_yield- Coroutines overview —
co_yield,co_await,co_return, promise types std::views::iota— C++20 stateless integer sequence; prefer over generator when no state is neededstd::ranges::to— C++23 range-to-container conversion- Fold expressions —
std::ranges::fold_left(C++23) for accumulating generator output