Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Advanced
  • 20 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 8 of 20 minutes free
Free: A generator computes one value per request · std::generator is a lazy range, where the library ships it · The loop and the generator take turns, one value at a time · Practise: lazy sequences and their turns. With a pass: 2 in-depth parts and 3 interview answers, about 12 minutes.

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

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

  1. Question 1 of 2 A function returns the first million squares as a std::vector<std::uint64_t>. How many megabytes does the vector hold before its caller reads the first value?

    Type a number.

    A hint

    Each 64-bit value takes 8 bytes, and the vector holds all of them at once.

    Show the answer: Question 1 of 2

    Answer: 8 MB

    A million values of 8 bytes are 8,000,000 bytes, 8 MB, all computed and stored before the caller sees the first one, as ten million took 80 MB. A generator of the same squares keeps one frame, however many values the caller takes.

  2. Question 2 of 2 Which of these consumers is a good fit for a generator?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A tool that reads the first 20 lines of a 2 GB log file and stops

    A generator fits sequences that are large, endless or expensive and consumers that may stop early. Random access, sorting and second passes need the values stored, in a container.

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:

fib_std.cpp C++ · fib_std.cpp
// 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);
}
GCC 16 runs it; libc++ has no <generator> Python · std_session.py
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:

Timeline · 6 events in order Where std::generator comes from, and who ships it Walk through it in order, from the standard to the three toolchains this lesson runs, and check which one compiles fib_std.cpp.
  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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_of lets a std::generator hand on every element of another range, such as a recursive call.
  • std::views adaptors such as take and filter work on any generator.

Checkpoint

Check: std::generator and who ships it

  1. Question 1 of 2 for (auto n : fibonacci() | std::views::drop(10) | std::views::take(5)) starts from 0, 1, 1, 2, 3, 5, 8, 13, 21, 34. What is the first value the loop prints?

    Type a number.

    A hint

    drop(10) skips exactly the ten values listed.

    Show the answer: Question 1 of 2

    Answer: 55

    drop(10) throws away the first ten Fibonacci numbers, 0 to 34, so the first value that reaches the loop is the eleventh, 55. fib_std.cpp printed 55 89 144 233 377.

  2. Question 2 of 2 primes() | std::views::filter(ends in 3) | std::views::take(6) printed 3 13 23 43 53 73, although primes() never ends. Why does the loop stop?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: take(6) stops asking after six matches, so the filter pulls primes only until then

    The views pull values one at a time. filter asks primes() for the next prime until one ends in 3, and take(6) stops asking after the sixth match, so the endless body is resumed only as often as the pipeline needs.

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 for loop and a generator take turns: begin and ++ resume the generator, each co_yield suspends it and hands one value back.for loop(the consumer)fibonacci()(the generator's frame)1. call: create the frame,suspend before the body2. begin(): resume3. co_yield 0: suspend,the loop reads 04. ++it: resume5. co_yield 1: suspend,the loop reads 16. the loop stops: destroy the frame

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.

  1. Calling fibonacci() creates the generator's frame, and the generator suspends before its body runs, so nothing is computed yet.
  2. The loop calls begin(), which resumes the generator.
  3. The body runs until co_yield 0, which suspends it and makes 0 the current value; the loop's body reads it.
  4. The loop calls ++ on its iterator, which resumes the generator where it stopped.
  5. 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.
  6. 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:

Interactive · message by message The turns of fibonacci() under drop(10) and take(5) Step through the pipeline of fib_std.cpp. Arrows are hand-overs between the loop, the views and the generator; the sub-labels show what each holds.
The turns of fibonacci() under drop(10) and take(5), step 9 of 9. Messages so far: for loop to fibonacci(): fibonacci(); for loop to drop(10) | take(5): begin(); fibonacci() to drop(10) | take(5): 0; drop(10) | take(5) to for loop: 55; for loop to fibonacci(): ++ resumes; drop(10) | take(5) to fibonacci(): resume, 16th.for loopprinted 5 valuesdrop(10) | take(5)the views in betweenfibonacci()frame freedfibonacci()begin()055++ resumesresume, 16th

The drawing shows the whole scene; its 9 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. ++ 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.
  7. Five values printed. 89, 144, 233 and 377 follow the same way, one resume each, and the loop has printed its five values.
  8. 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.
  9. 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:

fib_resumes.cpp C++ · fib_resumes.cpp
// 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);
}
Sixteen co_yield lines for five printed values Python · resumes_session.py
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

  1. Question 1 of 2 How many times does the pipeline of fib_std.cpp resume fibonacci() for the five values the loop prints?

    Type a number.

    A hint

    Count drop's ten, the loop's five, and what the loop does after the fifth value.

    Show the answer: Question 1 of 2

    Answer: 16 resumes

    drop(10) needs ten values and the loop prints five, but after the fifth value the loop calls ++ once more, and take moves its iterator on before it checks its count. fib_resumes.cpp printed 16 co_yield lines, the last one 610, which nobody reads.

  2. Question 2 of 2 A loop over a generator stores &n for each value in a vector and prints through the pointers after the loop. What is wrong?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Each n is valid only until the generator resumes, so every stored pointer outlives its value

    A reference to a yielded value is valid only until the generator resumes, and the loop resumes it at every step. Copy the values into the container, push_back(n), when they must outlive the step that produced them.

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.

  1. Question 1 of 8 What does the memory a generator uses depend on?

    Choose one answer.

    Show the answer: Question 1 of 8

    Answer: Its frame alone, not how many values the consumer takes

    A generator computes one element per request and keeps only its frame: the locals of its loop and the current value. Ten million squares cost the same frame as ten.

  2. Question 2 of 8 In which of these cases is a generator a poor choice?

    Choose every answer that is right.

    Show the answer: Question 2 of 8

    Answer:

    • The consumer needs to read the values twice.
    • The consumer jumps to the element at index 900 directly.

    A generator is an input range: its values can be read once and in order. Random access and second passes need a container. Endless sequences and large files, read one value at a time, are what generators are for.

  3. Question 3 of 8 Which of these toolchains provide <generator> with std::generator?

    Choose every answer that is right.

    Show the answer: Question 3 of 8

    Answer:

    • GCC 16 with libstdc++

    GCC 16's libstdc++ provides std::generator and defines __cpp_lib_generator as 202207. libc++ does not ship the header, neither in Apple clang 21 nor in LLVM 23, so code for both needs its own generator type.

  4. Question 4 of 8 What does calling a generator function, such as primes(), do before anything iterates over it?

    Choose one answer.

    Show the answer: Question 4 of 8

    Answer: It creates the frame and suspends before the first statement of the body.

    The coroutine's frame and promise are created by the call, and the generator suspends at its initial suspend point, so none of the body runs. begin() resumes it for the first value.

  5. Question 5 of 8 fibonacci() contains while (true), yet for (auto n : fibonacci() | std::views::take(5)) ends. Why?

    Choose one answer.

    Show the answer: Question 5 of 8

    Answer: take(5) stops asking for values after five, and the loop then destroys the suspended generator.

    A generator computes a value only when the loop resumes it. take(5) reports the end of its range after five values, so the loop ends; the generator object goes out of scope while it is suspended inside the loop, and its destructor frees the frame without the body ever finishing.

  6. Question 6 of 8 Inside for (const std::string& name : names()), how long may your code use the reference name?

    Choose one answer.

    Show the answer: Question 6 of 8

    Answer: Until the loop's next step resumes the generator

    The value lives in the generator's frame, and it is destroyed or overwritten when the generator runs on to its next co_yield. Copy or move the value if it has to outlive the current step.

  7. Question 7 of 8 While the loop's body prints 55 in fib_std.cpp, what is fibonacci() doing?

    Choose one answer.

    Show the answer: Question 7 of 8

    Answer: It is suspended at co_yield, with 55 in its frame, until the next ++

    begin() and every ++ resume the generator; every co_yield suspends it and leaves the value in its frame. While the loop's body runs, the generator waits at co_yield.

  8. Question 8 of 8 Change take(5) to take(6) in fib_std.cpp. How many times is fibonacci() resumed now?

    Type a number.

    A hint

    With take(5) it was resumed 16 times, and the reason for the last one has not changed.

    Show the answer: Question 8 of 8

    Answer: 17 resumes

    drop(10) still needs ten values and the loop now prints six, and after the sixth the loop's ++ moves take's iterator on once more before take checks its count: 10 + 6 + 1 = 17 resumes, the last one computing 987.

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.

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.

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

  1. 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.

  2. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. 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?

    Model answer, Pro. First step and follow-ups

  4. 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.

    Model answer, Pro. First step and follow-ups

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 with for and std::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-written Generator<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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.