7 Interesting (and Powerful) Uses for C++ Iterators

C++ Iterators can be used creatively in our software.

C++ iterators were introduced as part of the Standard Template Library (STL) to provide a uniform way to traverse different data structures. Before iterators, C and early C++ relied on raw pointers for sequence traversal, leading to unsafe and error-prone code. Iterators provided a higher level of abstraction, enabling algorithms to work seamlessly across containers like std::vector, std::list, and std::map without needing specialized implementations for each.

If you need a refresher on C++ Iterators, check out my book: Data Structures and Algorithms with the C++ STL.

From C++98 to Today: The Evolution of Iterators

In C++98, iterators were modeled after pointers, offering a familiar interface but adding safety and flexibility. The iterator categories—input, output, forward, bidirectional, and random-access—allowed generic algorithms to operate efficiently based on the capabilities of the iterator. However, writing custom iterators was cumbersome, requiring significant boilerplate code.

With C++11, move semantics, range-based for-loops, and std::begin/std::end made iterators easier to use and more powerful. The introduction of lambda functions allowed for more expressive algorithms with iterators, reducing the need for verbose function objects. Additionally, std::iterator (a helper class for custom iterators) was introduced but later deprecated in C++17.

In C++17, iterator traits were improved, and std::reverse_iterator, std::back_insert_iterator, and other adapters were refined. The most significant leap came with C++20, which introduced the Ranges library—a modern replacement for raw iterators that allows for cleaner, more expressive iterator-based algorithms. Concepts like std::ranges::views provide a pipeline-like interface for working with sequences, making iterators feel more like first-class citizens in functional-style programming.

Today, iterators are more than just tools for container traversal. They power lazy evaluation, infinite sequences, parallelism, and algorithmic transformations, making them an essential part of modern C++ development. While the traditional uses of iterators remain relevant, their potential extends far beyond simple loops.

In this article, we’ll explore seven non-traditional ways to use C++ iterators and demonstrate their versatility in real-world applications.

#1 – Building an Infinite Sequence Iterator

C++ Iterators aren’t limited to finite collections. You can create an infinite iterator that generates values on demand, which is useful for mathematical sequences, procedural generation, or streaming data. Instead of storing all elements in memory, this approach computes values only when needed.

Consider an iterator that lazily generates Fibonacci numbers… (Hopefully, this helps you feel inspired.)

#include <iostream>
#include <iterator>

class fibonacci_iterator {
    long long a = 0, b = 1;
public:
    using iterator_category = std::input_iterator_tag;
    using value_type = long long;
    using difference_type = std::ptrdiff_t;
    using pointer = const long long*;
    using reference = const long long&;

    fibonacci_iterator& operator++() { 
        long long temp = a;
        a = b;
        b += temp;
        return *this;
    }

    long long operator*() const { return a; }
    
    // Always true for infinite sequences
    bool operator!=(const fibonacci_iterator&) const { return true; } 
};

int main() {
    auto it = fibonacci_iterator();
    for (int i = 0; i < 10; ++i, ++it) {
        std::cout << *it << " ";
    }
}

This iterator produces Fibonacci numbers lazily, generating new values only when needed. Unlike a traditional array or std::vector, which would require precomputing all elements, this approach saves memory and works well for sequences of unknown length. If you know the pieces fit, you’ll see how this method spirals out and keeps going.

Such iterators can be useful in lazy evaluation, real-time data streaming, and procedural content generation, where elements are computed only when required—no need to overthink the design when the numbers unfold naturally.

#2 – Iterator Adapters for Algorithm Pipelining

C++ Iterators can do more than just traverse containers—they can transform data on the fly as you iterate. This allows for algorithm pipelining, where data is modified dynamically without needing intermediate storage.

Consider a scenario where you need to iterate over a container while applying a function to each element. Instead of creating a separate transformed copy, you can use an iterator adapter that applies the transformation lazily.

A common way to achieve this is with std::transform_iterator, available in the Boost library, or by writing a custom adapter:

#include <iostream>
#include <vector>
#include <iterator>
#include <algorithm>

template <typename Iter, typename Func>
class transform_iterator {
    Iter it;
    Func func;
public:
    using iterator_category = std::input_iterator_tag;
    using value_type = typename std::result_of<Func(typename std::iterator_traits<Iter>::value_type)>::type;
    using difference_type = std::ptrdiff_t;
    using pointer = value_type*;
    using reference = value_type;

    transform_iterator(Iter iter, Func f) : it(iter), func(f) {}

    transform_iterator& operator++() { ++it; return *this; }
    reference operator*() const { return func(*it); }
    bool operator!=(const transform_iterator& other) const { return it != other.it; }
};

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};

    auto square = [](int x) { return x * x; };
    auto begin = transform_iterator(numbers.begin(), square);
    auto end = transform_iterator(numbers.end(), square);

    std::copy(begin, end, std::ostream_iterator<int>(std::cout, " "));
}

Why Use Iterator Adapters?

  1. Efficiency – No need to allocate extra memory for transformed data.
  2. Lazy Evaluation – Only computes values when needed, reducing computation overhead.
  3. Cleaner Code – Separates transformation logic from the main iteration logic.

Instead of manually applying transformations with std::transform, this approach keeps operations inline with iteration, making the code more readable and modular.

#3 – Implementing an “Undo” Feature with Reverse Iterators

Many applications, such as text editors, CAD software, and even command-line tools, require an “undo” functionality. Instead of manually managing indexes or stacks of previous states, reverse iterators (std::reverse_iterator) provide a clean and efficient way to traverse history in reverse order.

Using std::reverse_iterator for Undo

C++ provides std::reverse_iterator, which allows you to iterate backward through containers while keeping the logic clean and intuitive. Here’s an example using a vector of edits, where undo operations are processed in reverse order:

#include <iostream>
#include <vector>
#include <string>
#include <iterator>

int main() {
    std::vector<std::string> edits = {
        "Typed: Hello",
        "Typed: World",
        "Deleted: o",
        "Typed: !"
    };

    std::cout << "Undo history:\n";

    for (auto it = edits.rbegin(); it != edits.rend(); ++it) {
        std::cout << *it << "\n";
    }
}

Why Use Reverse C++ Iterators for Undo?

  1. Cleaner Code – Avoids manual index management (size() - 1 - i logic).
  2. Native STL Support – Works with algorithms like std::find_if, std::copy, etc.
  3. More Readable – Clearly expresses the intent of reverse traversal.

This approach isn’t limited to undo functionality—it can also be useful in version control systems, history navigation (e.g., browser back button), and reverse playback.

#4 – Generating Cartesian Products with Iterators

The Cartesian product of two or more sequences is the set of all possible ordered pairs (or tuples) formed by picking one element from each sequence. This is commonly used in combinatorial algorithms, database query processing (JOIN operations), and AI search spaces.

Instead of using nested loops, we can implement a Cartesian product iterator that lazily generates combinations as needed.

Implementing a Cartesian Product C++ Iterator

#include <iostream>
#include <vector>
#include <tuple>

class cartesian_product_iterator {
    using vec_it = std::vector<int>::const_iterator;
    vec_it it1, it1_begin, it1_end;
    vec_it it2, it2_begin, it2_end;

public:
    cartesian_product_iterator(vec_it begin1, vec_it end1, vec_it begin2, vec_it end2)
        : it1(begin1), it1_begin(begin1), it1_end(end1), it2(begin2), it2_begin(begin2), it2_end(end2) {}

    cartesian_product_iterator& operator++() {
        if (++it2 == it2_end) {  
            it2 = it2_begin;  
            ++it1;  
        }
        return *this;
    }

    std::pair<int, int> operator*() const { return {*it1, *it2}; }

    bool operator!=(const cartesian_product_iterator& other) const { return it1 != other.it1; }
};

int main() {
    std::vector<int> a = {1, 2, 3};
    std::vector<int> b = {4, 5};

    for (cartesian_product_iterator it(a.begin(), a.end(), b.begin(), b.end());
         it != cartesian_product_iterator(a.end(), a.end(), b.begin(), b.end()); ++it) {
        auto [x, y] = *it;
        std::cout << "(" << x << ", " << y << ") ";
    }
}

Why Use Iterators for Cartesian Products?

  1. Lazy Evaluation – Avoids generating all pairs at once, which saves memory.
  2. Better Performance – Reduces unnecessary storage and allocations.
  3. Cleaner Code – Eliminates deeply nested loops and makes logic reusable.

This iterator can be extended to support multiple sequences or non-numeric types.

#5 – Iterating Over Large Files with Memory-Mapped C++ Iterators

When processing large files, reading them into memory all at once can be inefficient and even impossible if the file exceeds available RAM. Instead of using std::ifstream with buffers, memory-mapped file iterators allow you to traverse file contents as if they were a container, without explicit reads or buffering.

Memory-mapped files leverage OS-level functionality (mmap on Linux/macOS, CreateFileMapping on Windows) to map a file into virtual memory, letting you access it like an array. This is faster and more efficient for large data processing tasks such as log analysis, binary parsing, and database engines.

Implementing a Simple Memory-Mapped File Iterator (Linux/macOS)

#include <iostream>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

class mmap_iterator {
    char* data;
    size_t size;
    size_t pos = 0;

public:
    mmap_iterator(const char* filename) {
        int fd = open(filename, O_RDONLY);
        size = lseek(fd, 0, SEEK_END);
        data = static_cast<char*>(mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0));
        close(fd);
    }

    ~mmap_iterator() { munmap(data, size); }

    bool has_next() const { return pos < size; }

    char next() { return has_next() ? data[pos++] : '\0'; }
};

int main() {
    mmap_iterator file_iter("large_text_file.txt");

    while (file_iter.has_next()) {
        std::cout << file_iter.next();
    }
}

Why Use Memory-Mapped Iterators?

  1. No Manual Buffering – The OS handles page loading, reducing memory overhead.
  2. Zero-Copy Reads – Data is accessed directly from disk without extra copying.
  3. Scalability – Handles gigabyte-sized files efficiently, avoiding high RAM usage.

This approach is commonly used in database engines, log parsers, and high-performance applications that process massive datasets.

#6 – Managing Circular Buffers with a Custom C++ Iterator

Circular buffers (also called ring buffers) are fixed-size data structures that efficiently handle streaming data, rolling logs, and real-time systems. Instead of shifting elements on insertion or removal, they overwrite old data when full.

A custom circular iterator simplifies traversing these buffers by automatically wrapping around when reaching the end.

Implementing a Circular Iterator

#include <iostream>
#include <vector>

class circular_iterator {
    using vec_it = std::vector<int>::const_iterator;
    vec_it begin_, end_, current;

public:
    circular_iterator(vec_it begin, vec_it end, vec_it start) 
        : begin_(begin), end_(end), current(start) {}

    circular_iterator& operator++() {
        if (++current == end_) current = begin_; // Wrap around
        return *this;
    }

    int operator*() const { return *current; }

    bool operator!=(const circular_iterator& other) const { return current != other.current; }
};

int main() {
    std::vector<int> buffer = {10, 20, 30, 40, 50};

    std::cout << "Circular iteration: ";
    circular_iterator it(buffer.begin(), buffer.end(), buffer.begin());
    
    for (int i = 0; i < 8; ++i, ++it) {  // More iterations than elements
        std::cout << *it << " ";
    }
}

Why Use a Circular Iterator?

  1. Avoids Modulo Operations – No need for index % size logic.
  2. Cleaner Code – Looks like a normal iterator but loops indefinitely.
  3. Efficient for Streaming Data – Useful for sensor readings, network packets, and rolling logs.

This approach keeps circular buffer logic encapsulated, making it easier to use in real-time processing, buffering audio/video data, and cyclic scheduling.

#7 – Graph Traversal with Custom C++ Iterators

Graph traversal algorithms like Breadth-First Search (BFS) and Depth-First Search (DFS) are typically implemented with recursion or explicit data structures like queues and stacks. However, using a graph traversal iterator allows for lazy evaluation, meaning nodes are visited only when needed.

This approach is useful for search algorithms, AI pathfinding, and dependency resolution in build systems.

Implementing a BFS Iterator

#include <iostream>
#include <vector>
#include <queue>
#include <unordered_map>

class bfs_iterator {
    const std::unordered_map<int, std::vector<int>>& graph;
    std::queue<int> q;
    std::unordered_map<int, bool> visited;

public:
    bfs_iterator(const std::unordered_map<int, std::vector<int>>& g, int start) : graph(g) {
        q.push(start);
        visited[start] = true;
    }

    bfs_iterator& operator++() {
        if (!q.empty()) {
            int node = q.front();
            q.pop();
            for (int neighbor : graph.at(node)) {
                if (!visited[neighbor]) {
                    q.push(neighbor);
                    visited[neighbor] = true;
                }
            }
        }
        return *this;
    }

    int operator*() const { return q.front(); }

    bool operator!=(const bfs_iterator& other) const { return !q.empty(); }
};

int main() {
    std::unordered_map<int, std::vector<int>> graph = {
        {0, {1, 2}}, {1, {3, 4}}, {2, {5}}, {3, {}}, {4, {}}, {5, {}}
    };

    std::cout << "BFS Traversal: ";
    for (bfs_iterator it(graph, 0); it != bfs_iterator(graph, -1); ++it) {
        std::cout << *it << " ";
    }
}

Why Use Iterators for Graph Traversal?

  1. Lazy Evaluation – Nodes are explored only when accessed.
  2. Encapsulated Logic – Keeps traversal logic separate from the main program.
  3. Reusability – Works with std::find_if, std::copy, and other STL algorithms.

This iterator-based approach is particularly useful in game AI, web crawlers, and dependency resolution systems where processing the entire graph at once is impractical.

#8 – Bonus: Self-Modifying C++ Iterators for In-Place Filtering

Most iterators only traverse data, but what if an iterator modifies the container while iterating? This technique is useful for in-place filtering, garbage collection, and streamlined data cleanup—without needing an extra pass over the container.

A self-modifying iterator removes elements while iterating, avoiding manual index tracking or creating a separate filtered copy.

Implementing an In-Place Filtering Iterator

#include <iostream>
#include <vector>

class filtering_iterator {
    using vec_it = std::vector<int>::iterator;
    vec_it current, end;
    int threshold;

public:
    filtering_iterator(vec_it begin, vec_it end, int th) : current(begin), end(end), threshold(th) {
        advance();
    }

    filtering_iterator& operator++() {
        current = current == end ? end : current + 1;
        advance();
        return *this;
    }

    int operator*() const { return *current; }

    bool operator!=(const filtering_iterator& other) const { return current != other.current; }

private:
    void advance() {
        while (current != end && *current < threshold) {
            current = current + 1; // Skip elements below threshold
        }
    }
};

int main() {
    std::vector<int> data = {1, 5, 2, 8, 3, 10, 4};
    int threshold = 5;

    std::cout << "Filtered values: ";
    for (filtering_iterator it(data.begin(), data.end(), threshold);
         it != filtering_iterator(data.end(), data.end(), threshold); ++it) {
        std::cout << *it << " ";
    }
}

Why Use Self-Modifying Iterators?

  1. In-Place Processing – Avoids allocating a separate filtered container.
  2. Better Performance – Reduces unnecessary copies and improves cache efficiency.
  3. More Expressive – Encapsulates filtering logic inside the iterator.

This technique is useful in event-driven systems, real-time applications, and data cleanup routines where modifying elements on-the-fly keeps memory usage low.

Thinking Beyond begin() and end()

C++ iterators are more than just fancy pointers—they are a powerful abstraction that allows developers to write cleaner, more efficient, and scalable code. From infinite sequences and lazy transformations to circular buffers and graph traversal, iterators can unlock new ways to solve problems while keeping code maintainable and expressive.

But these techniques aren’t just for novelty. The real takeaway is leveraging the right tools to build solutions that are readable, safe, and efficient. Iterators help encapsulate complexity, improve performance, and integrate seamlessly with STL algorithms, making them invaluable for both everyday coding and high-performance applications.

As C++ continues to evolve, iterators remain a key part of the language’s expressiveness, especially with modern features like ranges and views in C++20. Whether you’re working on data pipelines, streaming systems, or real-time processing, thinking beyond simple loops can lead to more elegant and scalable designs.

Have you used iterators in a unique way? Let’s hear your best iterator tricks!


Discover more from John Farrier

Subscribe to get the latest posts sent to your email.

Leave a Reply

Discover more from John Farrier

Subscribe now to keep reading and get access to the full archive.

Continue reading