C++23 std::generator: Mastering Coroutine Patterns for Lazy Sequences
Learn C++23 std::generator and coroutine patterns with production-ready examples.
20+ years shipping performance-critical C and C++ systems. Drawn from code that ran under real load.
- ✓Basic knowledge of C++20 coroutines (co_yield, co_await).
- ✓Familiarity with C++ ranges library.
- ✓Understanding of RAII and lifetime management.
- std::generator is a C++23 coroutine type that produces lazy sequences.
- It simplifies writing iterators and state machines using co_yield.
- Coroutines are stackless; generators suspend/resume efficiently.
- Use co_yield to emit values; co_return for finalization.
- Combine with ranges for composable, lazy pipelines.
Imagine a vending machine that dispenses one snack at a time when you press a button. You don't need to know how it restocks or manages inventory; you just get the next item. std::generator is like that: it produces values on demand, pausing between each, without loading everything into memory at once.
In modern C++, generating sequences lazily is a common need—whether you're processing large datasets, implementing infinite streams, or building state machines. Before C++23, you had to write custom iterators or use external libraries. With C++23's std::generator, you can write coroutines that produce values on the fly using co_yield, making code concise and efficient.
Coroutines are functions that can suspend execution and resume later, preserving local state. std::generator is a coroutine return type that models an input range: it yields values one at a time. This tutorial dives deep into std::generator and coroutine patterns, covering syntax, lifetime, error handling, and production debugging.
You'll learn how to build lazy sequences, combine generators, and avoid common pitfalls. We'll also explore a real-world incident where a generator caused a production outage due to dangling references. By the end, you'll be ready to use std::generator in your own high-performance C++ projects.
What is std::generator?
std::generator is a C++23 coroutine return type defined in <generator>. It represents a lazy sequence of values produced by a coroutine. When you iterate over a generator, the coroutine runs until it hits a co_yield expression, which suspends execution and returns a value. The next iteration resumes the coroutine from where it left off.
- Models std::ranges::input_range.
- Values are produced on demand (lazy).
- The coroutine frame is allocated on the heap (unless optimized).
- Supports yielding by value, reference, or rvalue reference.
- Can be combined with range adaptors for composability.
Here's a minimal example:
Coroutine Fundamentals for Generators
To understand std::generator, you need to know the coroutine machinery. A coroutine function is any function that contains co_yield, co_await, or co_return. The compiler transforms it into a state machine.
- promise_type: Defines how values are produced and the coroutine's lifecycle.
- coroutine_handle: A handle to the suspended coroutine.
- co_yield: Suspends the coroutine and returns a value to the caller.
- co_return: Finalizes the coroutine (optional).
std::generator provides a default promise_type that handles co_yield. You can customize it by specializing std::generator::promise_type.
Example with explicit promise_type:
Yielding Values: By Value, Reference, and Rvalue
std::generator supports three yield modes: - By value: co_yield value; copies or moves the value. - By reference: co_yield std::ref(value); yields a reference. - By rvalue: co_yield std::move(value); moves the value.
By value is safest. By reference can lead to dangling if the referenced object is destroyed. By rvalue is useful for transferring ownership.
Example demonstrating all three:
Combining Generators with Ranges
std::generator models std::ranges::input_range, so you can use it with range adaptors like views::transform, views::filter, etc. This enables lazy composability.
Example: Generate Fibonacci numbers and filter evens:
Error Handling in Generators
Exceptions thrown inside a generator coroutine are propagated to the caller when the generator is iterated. However, if an exception escapes the coroutine without being caught, it can lead to undefined behavior. Always catch exceptions inside the coroutine or ensure they are handled.
Example with try-catch:
Lifetime and Ownership Patterns
Generators hold state in a heap-allocated coroutine frame. The generator object is a lightweight handle to this frame. When the generator is destroyed, the coroutine is destroyed (if not already completed).
- Generator as member: Ensure the generator object outlives any references it yields.
- Generator in containers: Moving a generator invalidates the original; use std::move.
- Generator with shared state: Use shared_ptr to extend lifetime.
Example of a generator that yields from a container:
Performance Considerations
Generators have overhead due to heap allocation and state machine jumps. For tight loops, consider alternatives like raw iterators or ranges. However, for complex sequences, generators can be more readable and maintainable.
Benchmark example:
The Dangling Generator: A Production Outage
- Never yield references to local variables in generators.
- Ensure the generator object outlives all references it yields.
- Use value semantics unless you have a clear lifetime guarantee.
- Test with sanitizers (ASan, UBSan) to catch dangling references.
- Document ownership and lifetime expectations for coroutine objects.
g++ -fsanitize=address -std=c++23valgrind ./a.out| File | Command / Code | Purpose |
|---|---|---|
| minimal_generator.cpp | std::generator | What is std |
| custom_promise.cpp | struct MyPromise { | Coroutine Fundamentals for Generators |
| yield_modes.cpp | std::generator | Yielding Values |
| generator_ranges.cpp | std::generator | Combining Generators with Ranges |
| error_handling.cpp | std::generator | Error Handling in Generators |
| lifetime.cpp | std::generator | Lifetime and Ownership Patterns |
| benchmark.cpp | std::generator | Performance Considerations |
Key takeaways
Common mistakes to avoid
4 patternsYielding a reference to a local variable.
Forgetting to include <generator>.
Using co_yield outside a coroutine.
Assuming generators are eager.
Interview Questions on This Topic
Explain how std::generator works under the hood.
Frequently Asked Questions
20+ years shipping performance-critical C and C++ systems. Drawn from code that ran under real load.
That's C++ Advanced. Mark it forged?
3 min read · try the examples if you haven't