C++ Module 17 – Modules and coroutines
Generators: std::generator and writing your own
Write lazy sequences with co_yield and C++23 std::generator, see which libraries ship it, and build a Generator<T> that works everywhere.
What you will learn
- Write lazy sequences with co_yield
- Use std::generator where the standard library provides it
- Write a small Generator<T> that works on every compiler
- Combine generators with range views
- Avoid dangling references to yielded values
Before you start
What you’ll learn · Before you start
What you will learn
- Write lazy sequences with co_yield
- Use std::generator where the standard library provides it
- Write a small Generator<T> that works on every compiler
- Combine generators with range views
- Avoid dangling references to yielded values
Before you start
On this page
A std::vector of the first ten million squares holds 80 megabytes before its caller reads one of them; a generator
of the same squares keeps one frame, however many values the caller takes. A generator: A coroutine that produces a sequence lazily: each co_yield hands one value to the code that consumes it and suspends until that code asks for the next one. C++23's std::generator<T> is such a type, and also a range. is
a coroutine that produces a sequence lazily: each co_yield hands one value to the loop that consumes it and pauses
until the loop asks for the next one, so it can even describe an endless sequence, such as every Fibonacci number,
and leave it to the consumer how many to take.
C++23 provides std::generator<T> in <generator>, a ready-made generator type that is also a range, so it works
in a range-based for loop and with std::views. Library support is uneven: GCC 16’s libstdc++ has it, while the
libc++ of Apple clang 21, and even that of LLVM 23, does not. Code that must build everywhere can use a short
hand-written Generator<T> instead.
A generator computes one value per request
A function that returns a std::vector computes every element before the caller sees the first one, and holds them
all in memory at once. A generator computes one element per request, so its memory stays constant, the first value
arrives at once, and a consumer that stops early never pays for the rest. That suits sequences that are large,
endless or expensive: the lines of a big file, test inputs, the nodes of a tree in traversal order, prime numbers. The
generator also keeps the producing code a plain loop, because the coroutine frame remembers where the loop was, which
a hand-written iterator class would have to store in members. Here are the ten million squares both ways:
| Ten million 64-bit squares | Returned in a std::vector |
Yielded by a generator |
|---|---|---|
| Memory before the first value | 80 MB, every value computed first | One frame |
| When the first value arrives | After all ten million | At once |
| A consumer that stops after 5 | Has paid for ten million | Has paid for 5, or 6 through take(5) |
| A second pass, or random access | Yes | No: each value is read once, in order |
The last row is what laziness costs. A generator is the wrong tool when the consumer needs random access or a second pass, or in the innermost loop of hot code, where a resume per element costs more than an index increment.
Checkpoint
Check: One value per request
std::generator is a lazy range, where the library ships it
Here are two endless sequences written with std::generator and cut short by views, built with GCC 16 and with both
clang toolchains:
// Lazy sequences with C++23's std::generator: nothing is computed until the loop asks for the next value.
#include <cstdint>
#include <generator>
#include <print>
#include <ranges>
#include <utility>
std::generator<std::uint64_t> fibonacci() { // endless: the consumer decides how many to take
std::uint64_t a = 0, b = 1;
while (true) {
co_yield a;
a = std::exchange(b, a + b);
}
}
std::generator<int> primes() {
for (int n = 2;; ++n) {
bool prime = true;
for (int d = 2; d * d <= n; ++d)
if (n % d == 0) prime = false;
if (prime) co_yield n;
}
}
int main() {
for (auto n : fibonacci() | std::views::drop(10) | std::views::take(5)) std::print("{} ", n);
std::println();
for (int p : primes() | std::views::filter([](int p) { return p % 10 == 3; }) | std::views::take(6)) std::print("{} ", p);
std::println();
std::println("__cpp_lib_generator = {}", __cpp_lib_generator);
} from session import run
run("""
g++-16 -std=c++23 -Wall -Wextra fib_std.cpp -o fib && ./fib
# The libc++ of Apple clang 21, and of LLVM 23, has no <generator>:
clang++ -std=c++23 fib_std.cpp -o fib-clang
/opt/homebrew/opt/llvm/bin/clang++ -std=c++23 -stdlib=libc++ fib_std.cpp -o fib-llvm
""") Output
$ g++-16 -std=c++23 -Wall -Wextra fib_std.cpp -o fib && ./fib
55 89 144 233 377
3 13 23 43 53 73
__cpp_lib_generator = 202207
# The libc++ of Apple clang 21, and of LLVM 23, has no <generator>:
$ clang++ -std=c++23 fib_std.cpp -o fib-clang
fib_std.cpp:3:10: fatal error: 'generator' file not found
3 | #include <generator>
| ^~~~~~~~~~~
1 error generated.
[exit status 1]
$ /opt/homebrew/opt/llvm/bin/clang++ -std=c++23 -stdlib=libc++ fib_std.cpp -o fib-llvm
fib_std.cpp:3:10: fatal error: 'generator' file not found
3 | #include <generator>
| ^~~~~~~~~~~
1 error generated.
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 std_session.py
fibonacci() loops forever, yet the program ends, because std::views::drop(10) and std::views::take(5) need only
fifteen of its values (one more is computed, as the next section counts), and the loop then destroys the generator. Calling fibonacci() by itself runs none of its
body: a std::generator starts at its first begin(), so an unused generator costs only its frame. primes() is
endless too, and the filter pulls values from it until six of them end in 3. GCC 16 defines __cpp_lib_generator as
202207, the value of the C++23 feature. Both clang builds stop at the #include, because neither libc++ ships the
header. Walk through where the type comes from and which toolchains ship it:
- C++20
Coroutines, but no generator type language
P0912R5 merged the Coroutines TS into C++20: co_await, co_yield and co_return, with building blocks such as std::coroutine_handle and std::suspend_always. The library had no type that a generator could return.
- C++23
std::generator in <generator> library
P2502R2 added std::generator: a move-only view whose iterator resumes the coroutine, and std::ranges::elements_of, which hands on the elements of a nested range. Its feature-test macro, __cpp_lib_generator, is 202207.
- GCC 14
libstdc++ adds it, as experimental
The GCC 14 release notes list "the std::generator view for getting results from coroutines" under improved experimental support for C++23.
- GCC 16
Compiles fib_std.cpp works
GCC 16 with libstdc++ builds the example, prints the Fibonacci and prime values, and reports __cpp_lib_generator = 202207.
- Apple clang 21
No <generator> in its libc++ missing
Apple clang 21 stops at the #include: fatal error, the file generator is not found. Code for Apple platforms needs its own generator type.
- LLVM 23
Not in LLVM's own libc++ either missing
Homebrew's LLVM 23 with -stdlib=libc++ fails the same way, and the libc++ C++23 status page lists P2502R2 with neither a status nor a first release.
The timeline has 6 events, from C++20 to LLVM 23.
The rest of this lesson builds on four pieces:
std::generator<T>is the C++23 library type: a move-only range whose iterator resumes the coroutine.- A hand-written
Generator<T>is about seventy lines of promise, iterator and sentinel that compile wherever coroutines do. std::ranges::elements_oflets astd::generatorhand on every element of another range, such as a recursive call.std::viewsadaptors such astakeandfilterwork on any generator.
Checkpoint
Check: std::generator and who ships it
The loop and the generator take turns, one value at a time
Follow one range-based for loop over fibonacci() in the diagram, from the call to the frame’s destruction:
A generator and the loop that consumes it take turns
Text description of the diagram
The diagram shows the two sides of a range-based for loop over fibonacci(), from top to bottom.
- Calling fibonacci() creates the generator's frame, and the generator suspends before its body runs, so nothing is computed yet.
- The loop calls begin(), which resumes the generator.
- The body runs until co_yield 0, which suspends it and makes 0 the current value; the loop's body reads it.
- The loop calls ++ on its iterator, which resumes the generator where it stopped.
- The body runs to the next co_yield, here 1, and suspends again; the loop reads the new value. Steps 4 and 5 repeat for every element.
- When the loop stops, because of take(5) or a break, the generator object goes out of scope and destroys the frame, even though the body never finished.
At any moment only one of the two runs, and the value the loop reads lives in the generator's frame until the next resume.
The two sides never run at the same time. begin() and every ++ resume the generator; every co_yield suspends it
and leaves the current value in its frame. Step through the pipeline of fib_std.cpp, turn by turn:
The drawing shows the whole scene; its 9 steps are listed below.
Every step as text
- The call: nothing runs. fibonacci() creates the generator's frame and suspends before its body. A std::generator starts at its first begin(), so a generator nobody iterates costs only its frame.
- begin() goes to the views. The for loop calls begin() on the pipeline. drop(10) has to skip ten values before it can hand out one, so it resumes the generator.
- co_yield 0, dropped. The body runs to co_yield a with a = 0 and suspends, leaving 0 in its frame. drop(10) reads it, throws it away and resumes the generator again.
- Nine more, dropped. Nine more resumes yield 1, 1, 2, 3, 5, 8, 13, 21 and 34, and drop(10) throws each away. Every resume runs one pass of the while (true) loop.
- The 11th resume: 55. The eleventh resume yields 55. take(5) passes it on, and the loop prints 55 while the value still sits in the generator's frame.
- ++ resumes it again. The loop's ++ resumes the generator, which moves on to co_yield 89. From this moment a reference to the 55 would be stale: the generator has replaced it.
- Five values printed. 89, 144, 233 and 377 follow the same way, one resume each, and the loop has printed its five values.
- One more: 610. After the fifth value the loop calls ++ once more, and take moves its iterator on before it checks its count: the generator is resumed a 16th time and computes 610, which nobody reads.
- Destroyed while suspended. take(5) now reports the end, the for loop ends, and the pipeline is destroyed with the generator in it. Its destructor frees the frame while the body is still suspended inside while (true).
The same program with every turn printed shows the count:
// The pipeline of fib_std.cpp, with every turn printed: the generator's co_yield, then the loop's body.
#include <cstdint>
#include <generator>
#include <print>
#include <ranges>
#include <utility>
int yields = 0; // how many times the body reached co_yield
std::generator<std::uint64_t> fibonacci() {
std::uint64_t a = 0, b = 1;
while (true) {
++yields;
std::println(" co_yield {}", a);
co_yield a;
a = std::exchange(b, a + b);
}
}
int main() {
for (auto n : fibonacci() | std::views::drop(10) | std::views::take(5)) std::println("loop: {}", n);
std::println("co_yield ran {} times for the 5 values the loop printed", yields);
} from session import run
run("""
g++-16 -std=c++23 -Wall -Wextra fib_resumes.cpp -o resumes && ./resumes
""") Output
$ g++-16 -std=c++23 -Wall -Wextra fib_resumes.cpp -o resumes && ./resumes co_yield 0 co_yield 1 co_yield 1 co_yield 2 co_yield 3 co_yield 5 co_yield 8 co_yield 13 co_yield 21 co_yield 34 co_yield 55 loop: 55 co_yield 89 loop: 89 co_yield 144 loop: 144 co_yield 233 loop: 233 co_yield 377 loop: 377 co_yield 610 co_yield ran 16 times for the 5 values the loop printed
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 resumes_session.py
Sixteen resumes for five printed values: ten that drop skipped, the five the loop printed, and 610, computed because
take moves its iterator on before it checks its count. A generator whose values are expensive pays for that extra
one. The turns also give the one rule every generator user must know: a reference to a yielded value is valid only
until the generator resumes. Read it, copy it or move it before the loop’s next step, and never keep a pointer to
it. Copying into a container, names.push_back(name) inside the loop, is always safe, because the container then owns
its own copy.
Key idea
The loop and the generator take turns: begin() and every ++ run the body to its next co_yield, and a value it
yields stays valid only until that next resume.
Checkpoint
Check: The loop and the generator take turns
Practise: lazy sequences and their turns
These questions check laziness, std::generator and the turns, beginning with whichever you have answered least well.
Practice that adapts to you
Practise this lesson
8 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
A Generator<T> for every compiler, line by line Pro: opens with a Pro, Premium or Ultimate pass
std::generator is missing wherever libc++ is the library, so portable code needs its own generator type. This part
builds one in about seventy lines, checks it against the range concepts, runs it on both compilers, and compares it
with the standard type.
In this part:
- The promise: a pointer to the yielded value
- The iterator and the sentinel: what makes it a range and a view
- What std::generator does that Generator<T> does not: the trade-offs
- What a yielded value costs, copies counted
- Your turn: stream the lines of a file lazily
9 runnable examples (C++, C++ header, CSV, Python) · about 7 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Lifetimes, allocations and recursion, measured Pro: opens with a Pro, Premium or Ultimate pass
Generators are cheap and safe when used as intended, and three measurements show where that stops: references kept too long, the frame’s allocation, and recursion. Each one is run and counted below.
In this part:
- References kept after the generator moves on, reproduced
- Does a generator allocate? Frame allocations counted
- Recursion: elements_of against nested loops, counted
- How deep a recursive generator can go, measured
1 diagram · 8 runnable examples (C++, C++ (deep_own.cpp), C++ (deep_std.cpp), C++ (recursion_own.cpp), C++ (recursion_std.cpp), Python) · about 7 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid · Warm-up
What is a generator in C++, and when would you return one instead of a std::vector?
A generator is a coroutine that produces a sequence lazily: each co_yield hands one value to the consumer and suspends until the consumer asks for the next one. C++23's std::generator<T> is such a type and also a range, so it works in a range-based for loop and with std::views. Return a generator when the sequence is endless, very large or expensive to compute, or when consumers often stop early: memory stays constant and the first value arrives at once. Return a vector when the caller needs random access, the size, or several passes over the data, because a generator's values can be read only once and in order. A reference to a yielded value is valid only until the generator resumes, so copy values that must be kept.
-
Mid to senior
A colleague's code stores pointers to the strings a generator yields in a vector inside the loop, then prints them after the loop. It printed the right names in a few tests and garbage in others. Explain what happens and how you would fix and catch it.
-
Senior to staff
Your code base must build with GCC 16 and with Apple clang, whose library has no <generator>. How would you provide generators, and what must a hand-written Generator<T> get right in its promise, its iterator and its ownership to be safe and work with std::views?
-
Senior to staff
A tree traversal written as a recursive generator, where each node's generator loops over its children's generators and re-yields their values, is slow on large trees and crashes on very deep ones. Explain both problems with numbers and give two fixes.
Key takeaways
- A generator computes one value per request, so its memory stays constant and a consumer that stops early never pays for the rest; it gives no random access and no second pass.
std::generator<T>(C++23) is a move-only range that works withforandstd::views; GCC 16 has it, libc++ does not.- The loop and the generator take turns, and a reference to a yielded value is valid only until the generator resumes.
- Where the library lacks
<generator>, a hand-writtenGenerator<T>of about seventy lines does the same job.
Terms in this lesson
- generator
- A coroutine that produces a sequence lazily: each co_yield hands one value to the code that consumes it and suspends until that code asks for the next one. C++23's std::generator<T> is such a type, and also a range.
References
- P2502R2: std::generator: Synchronous Coroutine Generator for Ranges (ISO/IEC JTC1/SC22/WG21)
- Working Draft, Programming Languages: C++ (N4950), 26.8 Range generators (ISO/IEC JTC1/SC22/WG21)
- P0912R5: Merge Coroutines TS into C++20 working draft (ISO/IEC JTC1/SC22/WG21)
- P2415R2: What is a view? (ISO/IEC JTC1/SC22/WG21)
- libc++ C++23 Status (LLVM Project)
- GCC 14 Release Series: Changes, New Features, and Fixes (GNU Project)
- P0981R0: Halo: coroutine Heap Allocation eLision Optimization: the joint response (ISO/IEC JTC1/SC22/WG21)
- AddressSanitizer (Clang documentation) (LLVM Project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress