std::generator (C++23)
Simplified coroutine-based sequence generation
Coroutines Recap
C++20 introduced coroutines, but they required significant boilerplate to use effectively. C++23's std::generator provides a ready-to-use implementation.
// C++20: Writing a coroutine required custom promise types
// (50+ lines of boilerplate)
// C++23: std::generator handles everything
#include <generator>
#include <iostream>
std::generator<int> count(int start, int end) {
for (int i = start; i < end; ++i) {
co_yield i; // Yield value and suspend
}
// co_return is implicit at end
}
int main() {
for (int n : count(0, 5)) {
std::cout << n << " ";
}
// Output: 0 1 2 3 4
}How It Works
co_yield
Produce a value and suspend execution. Resume when next value is requested.
Lazy Evaluation
Values are computed on-demand. Infinite sequences are possible.
Range Compatible
Works with C++20 ranges out of the box. Use with views and algorithms.
Basic Generator Examples
1. Infinite Sequences
std::generator<int> fibonacci() {
int a = 0, b = 1;
while (true) {
co_yield a;
int next = a + b;
a = b;
b = next;
}
}
// Safe because of lazy evaluation
for (int n : fibonacci() | std::views::take(10)) {
std::cout << n << " ";
}
// Output: 0 1 1 2 3 5 8 13 21 342. Recursive Generators
std::generator<int> tree_traversal(Node* root) {
if (!root) co_return;
// co_yield from another generator (C++23)
for (int val : tree_traversal(root->left)) {
co_yield val;
}
co_yield root->value;
for (int val : tree_traversal(root->right)) {
co_yield val;
}
}
// Flatten nested structure
for (int val : tree_traversal(root)) {
process(val);
}3. File Line Reader
std::generator<std::string> read_lines(const std::string& path) {
std::ifstream file(path);
std::string line;
while (std::getline(file, line)) {
co_yield line; // Memory-efficient line-by-line reading
}
}
// Process large files without loading entirely into memory
for (const auto& line : read_lines("huge_file.txt")) {
if (line.contains("ERROR")) {
std::cout << "Found error: " << line << std::endl;
}
}Combining with Ranges
std::generator is a range, so it works seamlessly with C++20 views:
namespace rv = std::views;
// Generator + Ranges = Powerful composition
auto even_squares = fibonacci()
| rv::filter([](int n) { return n % 2 == 0; })
| rv::transform([](int n) { return n * n; })
| rv::take(5);
for (int n : even_squares) {
std::cout << n << " ";
}
// Output: 0 4 16 144 3600
// Complex pipeline
auto result = read_lines("data.csv")
| rv::filter([](const auto& line) { return !line.empty(); })
| rv::transform(parse_csv_row)
| rv::filter([](const auto& row) { return row.is_valid; })
| rv::take(100);
// Process only first 100 valid, non-empty rowsGenerator with Input
Generators can receive input via co_yield assignment:
std::generator<int> stateful_generator() {
int multiplier = 1;
int value = 0;
while (true) {
// co_yield can receive input!
if (auto input = co_yield value * multiplier) {
// Input received, update state
multiplier = *input;
}
++value;
}
}
// Usage
auto gen = stateful_generator();
auto it = gen.begin();
std::cout << *it << std::endl; // 0
it = gen.send_value(2); // Send 2, set multiplier
++it;
std::cout << *it << std::endl; // 2 (1 * 2)
++it;
std::cout << *it << std::endl; // 4 (2 * 2)Performance Considerations
- ✓Zero overhead abstraction - generators compile to efficient state machines
- ✓No heap allocation for simple generators (compiler optimization)
- ✓Laziness avoids computing unused values
- ⚠Coroutine frame may allocate on heap (implementation dependent)
- ⚠Slightly more overhead than hand-written iterators
Best Practices
- ✓Use generators for lazy sequences, especially infinite ones
- ✓Combine with ranges for composable data processing
- ✓Prefer over eagerly computing large collections
- ✗Don't use when you need random access (generators are sequential)
- ✗Avoid co_await in generators (use async generators for that)