std::generator

Lazy evaluation and coroutine-based sequences

🔄 What is std::generator?

std::generator is a C++23 coroutine-based feature that creates lazy sequences of values. It generates values on-demand, making it memory-efficient for large or infinite sequences without storing all values at once.


#include <generator>
#include <iostream>

std::generator<int> fibonacci() {
    int a = 0, b = 1;
    while (true) {
        co_yield a;
        auto temp = a + b;
        a = b;
        b = temp;
    }
}

int main() {
    auto fib = fibonacci();
    for (int i = 0; i < 5; ++i) {
        std::cout << fib() << " ";
    }
}
                                    

Output:

0 1 1 2 3

Key Features of std::generator

💤

Lazy Evaluation

Values computed only when needed

co_yield value;
🔄

Coroutines

Built on C++20 coroutine foundation

// Pausable functions
💾

Memory Efficient

No need to store entire sequences

// Infinite sequences OK
🔗

Range Compatible

Works with C++20 ranges and algorithms

std::ranges::take(gen, 10)

🔹 Simple Number Generator

A basic sequence generator yields values on demand using iteration protocols. In C++, this can be implemented via a coroutine using co_yield or a class with an overloaded operator(). Each call produces the next value in a sequence, such as integers from a range or terms from a mathematical series. Generators are memory-efficient for large or infinite sequences because they produce values lazily, only when requested, rather than pre-computing an entire collection upfront.

#include <generator>
#include <iostream>

std::generator<int> count_up_to(int max) {
    for (int i = 1; i <= max; ++i) {
        co_yield i;
    }
}

int main() {
    std::cout << "Counting to 5: ";
    for (int value : count_up_to(5)) {
        std::cout << value << " ";
    }
    std::cout << std::endl;
    
    return 0;
}

Output:

Counting to 5: 1 2 3 4 5

🔹 Infinite Sequence

Infinite sequences leverage lazy evaluation to generate unbounded data streams. Using coroutines or iterator adaptors, you can create generators that produce values indefinitely, like all Fibonacci numbers or a stream of random numbers. The key is that values are computed only as needed, preventing memory exhaustion. These sequences are useful for simulations, real-time data feeds, mathematical conjectures, and scenarios where the upper bound is unknown or conceptually infinite.

#include <generator>
#include <iostream>
#include <ranges>

std::generator<int> powers_of_two() {
    int power = 1;
    while (true) {
        co_yield power;
        power *= 2;
    }
}

int main() {
    auto powers = powers_of_two();
    
    std::cout << "First 8 powers of 2: ";
    for (int value : powers | std::views::take(8)) {
        std::cout << value << " ";
    }
    std::cout << std::endl;
    
    return 0;
}

Output:

First 8 powers of 2: 1 2 4 8 16 32 64 128

🔹 String Generator

Generators can produce sequences of strings or other complex types efficiently. For example, a generator might yield formatted log lines, permutations of words, or lines read lazily from a file. By returning strings on-the-fly, the generator avoids allocating a large contiguous container, reducing memory overhead. This pattern is ideal for text processing, template rendering, lexing, and any application where string data is produced incrementally and can be processed in a streaming fashion.

#include <generator>
#include <iostream>
#include <string>
#include <vector>

std::generator<std::string> word_generator(const std::vector<std::string>& words) {
    for (const auto& word : words) {
        co_yield "Hello, " + word + "!";
    }
}

int main() {
    std::vector<std::string> names = {"Alice", "Bob", "Charlie"};
    
    for (const auto& greeting : word_generator(names)) {
        std::cout << greeting << std::endl;
    }
    
    return 0;
}

Output:

Hello, Alice!
Hello, Bob!
Hello, Charlie!

🔹 Recursive Generator

Recursive generators create sequences defined by self-referential rules. They are particularly useful for traversing recursive data structures like trees or graphs, or for generating fractals and recursive sequences (e.g., directory listings). In C++, this can be achieved by having a generator co_yield values from its own recursive calls. Proper implementation manages the call stack elegantly and yields results in the desired order (depth-first or breadth-first) while maintaining clarity and performance.

#include <generator>
#include <iostream>

std::generator<int> tree_traversal(int depth, int value = 1) {
    co_yield value;
    
    if (depth > 0) {
        // Generate left subtree
        for (int left : tree_traversal(depth - 1, value * 2)) {
            co_yield left;
        }
        
        // Generate right subtree  
        for (int right : tree_traversal(depth - 1, value * 2 + 1)) {
            co_yield right;
        }
    }
}

int main() {
    std::cout << "Binary tree traversal (depth 2): ";
    for (int node : tree_traversal(2)) {
        std::cout << node << " ";
    }
    std::cout << std::endl;
    
    return 0;
}

Output:

Binary tree traversal (depth 2): 1 2 4 5 3 6 7

🧠 Test Your Knowledge

What keyword is used to yield values in a generator?