Generators (std::generator)
Generators (std::generator<T>) and Synchronous Yielding
Section titled “Generators (std::generator<T>) and Synchronous Yielding”This section covers C++23’s std::generator<T>, co_yield as syntactic sugar, lazy evaluation Semantics, a comparison with Python generators, and a complete Fibonacci generator example with Composable pipeline operations.
std::generator<T> (C++23)
Section titled “std::generator<T> (C++23)”C++23 introduced std::generator<T> [N4950 §25.4.4], a standard library type that wraps a coroutine Producing a sequence of values via co_yield. It is an input iterator whose operator++ resumes the coroutine and whose operator* returns the yielded value.
The declaration (simplified) [N4950 §25.4.4]:
template<class Ref, class V = remove_cvref_t<Ref>, class Allocator = allocator<char>>class generator : public ranges::view_interface<generator<Ref, V, Allocator>> { // ...};std::generator<T> is a view, it is lightweight, non-owning, and models input_range. Values are computed lazily on demand.
co_yield as Syntactic Sugar
Section titled “co_yield as Syntactic Sugar”The expression co_yield expr is defined by the standard as [N4950 §8.5.5]:
This means that co_yield requires the promise type to have a yield_value method that returns an Awaiter. The std::generator promise type internally stores the yielded value and returns a std::suspend_always awaiter to suspend the coroutine until the caller requests the next value.
Lazy Iteration
Section titled “Lazy Iteration”The defining characteristic of a generator is lazy evaluation. No values are computed until the Iterator is advanced. This makes generators suitable for representing potentially infinite Sequences, large data pipelines, or expensive computations where only a prefix of the results is Needed.
The memory usage of a generator is where is the depth of the coroutine’s local Variable state that crosses a suspend point, constant and independent of the number of Values produced.
Comparison with Python Generators
Section titled “Comparison with Python Generators”| Feature | Python generator | C++ std::generator<T> (C++23) |
|---|---|---|
| Syntax | yield expr | co_yield expr |
| Return type | Implicit | std::generator<T> or custom |
| Type safety | Dynamically typed | Statically typed (templates) |
| Lazy evaluation | Yes | Yes |
Composable via yield from | Yes (yield from gen) | Via nested coroutine calls or range adapters |
| Exception propagation | throw inside generator caught by caller | Same, exceptions propagate through co_await |
| Standard library support | Built-in since Python 2.2 | C++23 (<generator>) |
Complete Example: Fibonacci Generator with std::generator
Section titled “Complete Example: Fibonacci Generator with std::generator”#include <generator>#include <iostream>#include <cstdint>
std::generator<std::uint64_t> fibonacci() { std::uint64_t a = 0, b = 1; while (true) { co_yield a; auto next = a + b; a = b; b = next; }}
std::generator<std::uint64_t> take(std::generator<std::uint64_t> src, std::size_t n) { std::size_t count = 0; for (auto val : src) { if (count >= n) co_return; co_yield val; ++count; }}
std::generator<std::uint64_t> even_fibonacci() { for (auto val : fibonacci()) { if (val > 4'000'000) co_return; if (val % 2 == 0) co_yield val; }}
int main() { std::cout << "First 15 Fibonacci numbers:\n"; for (auto val : take(fibonacci(), 15)) { std::cout << " " << val << "\n"; }
std::cout << "\nEven Fibonacci numbers under 4,000,000:\n"; std::uint64_t sum = 0; for (auto val : even_fibonacci()) { std::cout << " " << val << "\n"; sum += val; } std::cout << "Sum: " << sum << "\n";}Output:
First 15 Fibonacci numbers: 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377
Even Fibonacci numbers under 4,000,000: 0 2 8 34 144 610 2584 10946 46368 196418 832040 3524578Sum: 4613732See Also
Section titled “See Also”- Stackless Coroutine Frames and Heap Allocation
- Coroutine Handle, Promise Type, and Awaiter
- Task Scheduling and Executors
Coroutine Generator Mechanism
Section titled “Coroutine Generator Mechanism”The std::generator type works through a promise_type that implements yield_value. Understanding this mechanism helps when writing custom generators or debugging generator behavior.
How yield_value Works
Section titled “How yield_value Works”When co_yield expr is executed, the coroutine:
- Evaluates
exprand passes it topromise.yield_value(expr). - The promise stores the value (by reference, copy, or move depending on the signature).
yield_valuereturns a suspend-awaiter (std::suspend_always), suspending the coroutine.- The caller advances the generator’s iterator via
++it. Which resumes the coroutine. - Execution continues after the
co_yielduntil the nextco_yield,co_returnor end of scope.
#include <generator>#include <iostream>#include <string>#include <coroutine>
// Custom generator with logging to show the mechanismtemplate<typename T>struct DebugGenerator { struct promise_type { T current_value; std::suspend_always yield_value(T value) { std::cout << " [promise] yield_value(" << value << ")\n"; current_value = std::move(value); return {}; }
DebugGenerator get_return_object() { std::cout << " [promise] get_return_object\n"; return DebugGenerator{Handle::from_promise(*this)}; }
std::suspend_always initial_suspend() noexcept { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() { throw; } };
using Handle = std::coroutine_handle<promise_type>; Handle handle;
struct sentinel {}; struct iterator { Handle handle;
iterator& operator++() { std::cout << " [iterator] ++it (resuming coroutine)\n"; handle.resume(); return *this; }
T& operator*() const { return handle.promise().current_value; }
bool operator==(sentinel) const { return handle.done(); } };
iterator begin() { std::cout << " [gen] begin() (resuming from initial_suspend)\n"; handle.resume(); return iterator{handle}; }
sentinel end() { return {}; }};
DebugGenerator<int> counting_generator(int start, int end) { std::cout << " [coroutine] starting\n"; for (int i = start; i <= end; ++i) { co_yield i; } std::cout << " [coroutine] done\n";}
int main() { std::cout << "Creating generator\n"; auto gen = counting_generator(1, 3); std::cout << "Iterating\n"; for (int val : gen) { std::cout << " Got: " << val << "\n"; }}Generator vs Iterator
Section titled “Generator vs Iterator”| Aspect | Manual Iterator | std::generator |
|---|---|---|
| State storage | Manual (member variables) | Automatic (coroutine frame) |
| Suspend/resume | Not supported | Built-in (co_yield / co_await) |
| Complexity | Boilerplate-heavy (begin``end``operator++``operator*) | Minimal, just write the body |
| Infinite sequences | Difficult (need sentinel tricks) | Natural (while(true) { co_yield ...; }) |
| Exception safety | Manual | Stack unwinding on unhandled exception |
| Composability | Limited | Nest coroutines, use range adaptors |
| Memory overhead | sizeof(iterator) | Coroutine frame ( ~100-300 bytes) |
Recursive Generators
Section titled “Recursive Generators”Generators can call themselves recursively. Each recursive invocation creates a new coroutine frame on the heap. This is useful for tree traversal, combinatorial generation, and recursive descent Parsing.
#include <generator>#include <iostream>#include <memory>#include <vector>
struct TreeNode { int value; std::vector<std::unique_ptr<TreeNode>> children;
explicit TreeNode(int v) : value{v} {}
void add_child(int v) { children.push_back(std::make_unique<TreeNode>(v)); }};
std::generator<int> traverse_dfs(TreeNode* node) { if (!node) co_return; co_yield node->value; for (auto& child : node->children) { for (int val : traverse_dfs(child.get())) { co_yield val; } }}
std::generator<int> traverse_bfs(TreeNode* root) { if (!root) co_return; std::vector<TreeNode*> level{root}; while (!level.empty()) { std::vector<TreeNode*> next; for (auto* node : level) { co_yield node->value; for (auto& child : node->children) { next.push_back(child.get()); } } level = std::move(next); }}
int main() { TreeNode root{1}; root.add_child(2); root.children[0]->add_child(4); root.children[0]->add_child(5); root.add_child(3); root.children[1]->add_child(6); root.children[1]->add_child(7); root.children[1]->add_child(8);
std::cout << "DFS: "; for (int val : traverse_dfs(&root)) { std::cout << val << " "; } std::cout << "\n";
std::cout << "BFS: "; for (int val : traverse_bfs(&root)) { std::cout << val << " "; } std::cout << "\n";}// Output:// DFS: 1 2 4 5 3 6 7 8// BFS: 1 2 3 4 5 6 7 8Composing Generators
Section titled “Composing Generators”Generators can be composed into pipelines. One generator can consume the output of another, and Range adapters can be inserted between them.
#include <generator>#include <iostream>#include <string>#include <string_view>#include <ranges>
namespace rv = std::views;
std::generator<std::string> lines(std::string_view text) { std::size_t start = 0; while (start < text.size()) { auto end = text.find('\n', start); if (end == std::string_view::npos) end = text.size(); co_yield std::string{text.substr(start, end - start)}; start = end + 1; }}
std::generator<std::string> non_empty(std::generator<std::string> src) { for (auto& line : src) { if (!line.empty()) { co_yield line; } }}
std::generator<std::string> trimmed(std::generator<std::string> src) { for (auto& line : src) { auto start = line.find_first_not_of(" \t"); auto end = line.find_last_not_of(" \t"); if (start == std::string::npos) { co_yield ""; } else { co_yield line.substr(start, end - start + 1); } }}
int main() { std::string_view text = " hello\n" "\n" " world \n" "\n" " foo\n";
std::cout << "Non-empty trimmed lines:\n"; for (const auto& line : trimmed(non_empty(lines(text)))) { std::cout << " [" << line << "]\n"; }}// Output:// Non-empty trimmed lines:// [hello]// [world]// [foo]Generator Performance Characteristics
Section titled “Generator Performance Characteristics”| Metric | Typical Value |
|---|---|
| Coroutine frame allocation | 1 heap allocation per std::generator creation |
| Frame size | ~100–300 bytes (depends on local variables) |
| Resume/suspend overhead | ~10–50ns (comparable to a virtual function call) |
| Memory usage (per element) | , no accumulation |
| Cache behavior | Poor if frame is large and accessed infrequently |
Practical Example: State Machine Generator
Section titled “Practical Example: State Machine Generator”#include <generator>#include <iostream>#include <charconv>#include <string_view>
enum class TokenKind { Number, Plus, Minus, Star, Slash, End };
struct Token { TokenKind kind; double value = 0.0;};
std::generator<Token> tokenize(std::string_view input) { std::size_t i = 0; auto skip_ws = [&] { while (i < input.size() && input[i] == ' ') ++i; };
while (i < input.size()) { skip_ws(); if (i >= input.size()) break;
char c = input[i]; if (c >= '0' && c <= '9') { double value = 0; auto [ptr, ec] = std::from_chars(input.data() + i, input.data() + input.size(), value); if (ec == std::errc{}) { i = static_cast<std::size_t>(ptr - input.data()); co_yield Token{TokenKind::Number, value}; } } else { switch (c) { case '+': co_yield Token{TokenKind::Plus}; break; case '-': co_yield Token{TokenKind::Minus}; break; case '*': co_yield Token{TokenKind::Star}; break; case '/': co_yield Token{TokenKind::Slash}; break; } ++i; } } co_yield Token{TokenKind::End};}
int main() { std::string_view expr = "3.14 + 42 * 7"; std::cout << "Tokens:\n"; for (const auto& tok : tokenize(expr)) { switch (tok.kind) { case TokenKind::Number: std::cout << " Number: " << tok.value << "\n"; break; case TokenKind::Plus: std::cout << " Plus\n"; break; case TokenKind::Minus: std::cout << " Minus\n"; break; case TokenKind::Star: std::cout << " Star\n"; break; case TokenKind::Slash: std::cout << " Slash\n"; break; case TokenKind::End: std::cout << " End\n"; break; } }}Common Pitfalls
Section titled “Common Pitfalls”- Dangling references in generators. If
co_yieldyields a reference to a local variable that is modified before the caller reads it, the caller sees the modified value. Always yield by value or ensure the referenced object is stable. - Forgetting to advance. A generator’s values are only computed when the iterator is advanced. If you create a generator but never iterate it, the coroutine body never executes.
- Exception propagation. If an exception is thrown inside a generator coroutine and not caught, it propagates to the caller on the next
++itcall. Always handle exceptions or document them. - Lifetime of captured references. If a generator coroutine captures a reference (via lambda or reference parameter), the referenced object must outlive the generator.
- Range adaptor eager materialization. Some range adaptors (like
std::views::reverse) may need to buffer elements, defeating the lazy evaluation benefit of generators.
Summary
Section titled “Summary”This topic covers the essential concepts and techniques related to generators (std::generator), including key principles and practical applications.
Key concepts include:
- core concepts and definitions
- key principles and frameworks
- practical applications
- common techniques and methods
- evaluation and critical analysis
Worked Examples
Section titled “Worked Examples”Intuition
Section titled “Intuition”Generators are like vending machines that dispense items one at a time. Instead of computing all possible values upfront (which might be infinite), a generator computes values on demand. When you ask for the next value, it runs until it produces one, then pauses. This is like a recipe that says “when you need the next cookie, bake it” instead of baking all cookies at once. The key insight is that generators enable lazy evaluation - you only compute what you need, which is crucial for infinite sequences or large datasets. Python’s yield and C++‘s co_yield both implement this pattern, though C++ adds more control over suspension behavior.
Cross-References
Section titled “Cross-References”- Stackless Coroutine Frames - How coroutine frames enable generator implementation
- Promise, Awaiter - How promise types control generator behavior