Skip to main content
    Courses/C++/Custom Iterators

    Lesson 25 • Advanced

    Custom Iterators

    By the end of this lesson you'll be able to make your own container work with range-based for and the STL algorithms — by writing a small iterator with operator*, operator++, and operator!=, and giving the container begin() and end().

    What You'll Learn

    • Write operator* to read (and write) the current element
    • Write operator++ to advance the cursor, returning *this
    • Write operator!= / operator== to detect the end of the range
    • Add begin() and end() so range-based for works
    • Add the five iterator_traits aliases for STL algorithms
    • Know when operator* returns by value vs by reference

    💡 Real-World Analogy

    An iterator is a finger you run down a shopping list. operator* is "read the line my finger is on". operator++ is "move my finger down one line". begin() puts your finger on the first line; end() is the blank space just past the last line — the spot that means "stop, you're done". The loop keeps asking "is my finger NOT yet at the blank space?" (operator!=) and, while the answer is yes, reads the line and moves down. Writing a custom iterator is just teaching the language how a finger moves over your data — a pointer in an array, a node-to-node hop in a linked list — and where the blank space is.

    📊 The Iterator Protocol

    PieceLives onJobReturns
    operator*iteratorRead the current elementT& (or T by value)
    operator++iteratorMove to the next element*this by reference
    operator!=iterator"Am I NOT at the end?"bool
    begin()containerCursor at the first elementan iterator
    end()containerSentinel one past the lastan iterator

    A range-based for (auto x : c) is pure sugar: the compiler rewrites it to for (auto it = c.begin(); it != c.end(); ++it) and calls *it each step. Provide those five pieces and your type "just works".

    1. A Container That Works With Range-Based for

    Here's a complete, correct example. IntBag stores five ints, and its nested Iterator is just a wrapper around a pointer into that array. Read every comment and run it. Notice the three iterator operators (*, ++, !=) and the container's begin()/end() — and that end() points one past the last element, never at a real value.

    Worked example: an iterable IntBag

    Read every comment, run it, and check the output matches.

    Try it Yourself »
    C++
    #include <iostream>
    using namespace std;
    
    // A tiny container: a fixed-size bag of ints that we own ourselves.
    // The goal is to make it work with range-based for and STL algorithms,
    // which means writing an ITERATOR — a little cursor over our data.
    class IntBag {
        int data_[5];                 // the storage (5 ints)
    public:
        IntBag(int a, int b, int c, int d, int e)
            : data_{a, b, c, d, e} {}
    
        // The Iterator is just a wrapper around a pointer into data_.
        class Iterator {
    ...

    2. Making STL Algorithms Accept Your Iterator

    Range-based for only needs the three operators. But std::sort, std::find, std::accumulate and friends also need to know what kind of iterator you have. They ask via std::iterator_traits, which reads five type aliases on your iterator. The most important is iterator_category — it declares the iterator's power level.

    🔎 Deep Dive: iterator categories & iterator_traits

    Each category is a promise about what an algorithm may do with your iterator. An input iterator is read-once, single-pass (like reading from a stream). A forward iterator can be read, written, and passed over more than once. A bidirectional iterator adds -- (like std::list). A random-access iterator adds + n and [] so you can jump anywhere (like std::vector) — and only that category works with std::sort.

    You declare your category with the five aliases below. Pick the weakest category your iterator honestly supports; claiming more than you implement leads to algorithms doing illegal things.

    class Iterator {
    public:
        using iterator_category = std::forward_iterator_tag;  // its power level
        using value_type        = int;        // what operator* yields
        using difference_type   = std::ptrdiff_t;             // it1 - it2
        using pointer           = int*;
        using reference         = int&;
        // ... operator*, operator++, operator!= ...
    };

    Add those aliases and the same begin()/end() now flow straight into the STL. Run this:

    Worked example: IntBag + STL algorithms

    The five aliases let accumulate, max_element, count_if and find accept your iterator.

    Try it Yourself »
    C++
    #include <iostream>
    #include <algorithm>     // std::find, std::count_if, std::max_element
    #include <numeric>       // std::accumulate
    using namespace std;
    
    class IntBag {
        int data_[5];
    public:
        IntBag(int a, int b, int c, int d, int e) : data_{a, b, c, d, e} {}
    
        class Iterator {
            int* ptr_;
        public:
            // These five type aliases are what std::iterator_traits reads so that
            // STL algorithms know HOW your iterator behaves. Forward iterators can
            // be read
    ...

    3. Your Turn: Wire Up the Three Operators

    Now you write the iterator. The WordList below is finished except for its three core operators. Fill in the blanks marked ___ using the // 👉 hints, then run it. Remember: operator* reads, operator++ advances and returns *this, and operator!= compares the two cursors.

    🎯 Your turn: WordList iterator

    Fill in the ___ blanks, then check your output against the expected line.

    Try it Yourself »
    C++
    #include <iostream>
    using namespace std;
    
    // 🎯 YOUR TURN — fill each ___ then press "Try it Yourself".
    // This container holds a small array. Wire up its Iterator so the
    // range-based for at the bottom prints every word.
    class WordList {
        const char* words_[3];
    public:
        WordList(const char* a, const char* b, const char* c)
            : words_{a, b, c} {}
    
        class Iterator {
            const char** ptr_;
        public:
            Iterator(const char** p) : ptr_(p) {}
    
            // 1) Return the value
    ...

    4. Your Turn: begin(), end() and the Sentinel

    A linked list has no array to point into — the iterator hops node to node by following next. The "one past the last" sentinel for a list is simply nullptr: when the cursor reaches it, the loop stops. Fill in the two blanks so the list becomes iterable.

    🎯 Your turn: make a linked list iterable

    Advance through nodes and return the right end() sentinel, then run and self-check.

    Try it Yourself »
    C++
    #include <iostream>
    using namespace std;
    
    // 🎯 YOUR TURN — fill each ___ so the linked list becomes iterable.
    // The Iterator walks node -> node by following the 'next' pointer.
    struct Node {
        int value;
        Node* next;
    };
    
    class IntList {
        Node* head_ = nullptr;
    public:
        void pushFront(int v) { head_ = new Node{v, head_}; }
    
        class Iterator {
            Node* cur_;
        public:
            Iterator(Node* n) : cur_(n) {}
            int operator*() const { return cur_->value; }
            // Move 
    ...

    🔎 Deep Dive: the const_iterator

    So far operator* returns T&, which lets callers write through the iterator (for (auto& x : bag) x += 1;). But what about a const container, or a loop you want to be read-only? For that you provide a const_iterator whose operator* returns const T&, plus cbegin()/cend() that return it. Real STL containers offer both.

    class ConstIterator {
        const int* ptr_;
    public:
        const int& operator*() const { return *ptr_; }  // read-only
        ConstIterator& operator++() { ++ptr_; return *this; }
        bool operator!=(const ConstIterator& o) const { return ptr_ != o.ptr_; }
    };
    // const IntBag b(...);  for (int x : b)  -> uses the const_iterator

    For your first custom iterator it is fine to ship the mutable version and add const_iterator later — but know that a container used in a const context needs one.

    Pro Tips

    • 💡 Implement !=, derive == from it: write operator!= for the loop, then bool operator==(const It& o) const { return !(*this != o); } for free.
    • 💡 Return *this from operator++: prefix increment must return the iterator by reference so the loop keeps stepping.
    • 💡 Match begin() and end() types: they must return the same iterator type, or it != end() won't compile.
    • 💡 Add the five aliases early: even a basic forward iterator needs them the moment you reach for an STL algorithm.

    Common Errors (and the fix)

    • Forgetting the end sentinel: if end() points at the last element instead of one past it, the loop stops one item early (or dereferences garbage). For an array use &data[size]; for a linked list use nullptr.
    • Off-by-one in the loop: the rule is half-open [begin, end)end() is never read. Using <=-style logic (stopping only after reading end) reads one element too many. Compare with != against the one-past sentinel.
    • Missing operator!=: "error: no match for 'operator!='" — range-based for can't compile without it. Add bool operator!=(const It& o) const (the loop calls it every step to test against end()).
    • Returning by value when you meant by reference: int operator*() const hands back a copy, so for (auto& x : c) x = 9; changes nothing. Return int& if callers must write through it; return by value only for generated values that aren't stored.
    • "error: no member named 'begin'": range-based for needs begin() and end() on the container (or free functions). Add both, returning the same iterator type.

    📋 Quick Reference

    NeedCodeNotes
    Read currentint& operator*() constreturn T& to allow writing
    AdvanceIt& operator++()return *this by reference
    Not finished?bool operator!=(const It& o)compare cursors
    First elementIt begin()cursor to data[0] / head
    One past lastIt end()&data[size] or nullptr
    STL categoryusing iterator_category = ...forward_iterator_tag etc.

    Frequently Asked Questions

    Q: What is the minimum an iterator needs to support range-based for?

    Just three operators plus begin()/end() on the container. The iterator must support operator* (read the current element), operator++ (move to the next element, prefix form, returning *this), and operator!= (compare against end so the loop knows when to stop). The container needs begin() returning an iterator to the first element and end() returning a 'one past the last' sentinel. With those five pieces, for (auto x : container) compiles and runs.

    Q: Why does end() point one past the last element instead of at the last element?

    It is the 'half-open range' convention [begin, end) that the whole STL uses. Pointing one past the last element means the loop condition is simply it != end(): when the cursor reaches that sentinel, you stop without ever dereferencing it. It also makes an empty container natural — begin() == end() with no special cases — and lets size be computed as end - begin. For a linked list the sentinel is usually nullptr; for an array it is &data[size].

    Q: Should operator* return by value or by reference?

    Return by reference (T&) when callers should be able to read AND write the element through the iterator — that is what lets for (auto& x : c) x = ...; modify the container, and it is what STL containers do. Return by value when the element is computed on the fly and does not exist as stored memory (for example a Range that generates numbers), because there is nothing to take a reference to. If you offer a const_iterator, its operator* returns const T& so the data cannot be changed.

    Q: What are iterator_category and iterator_traits for?

    std::iterator_traits is how STL algorithms ask 'what kind of iterator is this?'. It reads five type aliases on your iterator — iterator_category, value_type, difference_type, pointer, and reference. The category (input, forward, bidirectional, random_access) tells an algorithm what it is allowed to do: std::sort needs random access, std::find only needs forward. If you omit these aliases, many algorithms will fail to compile, so add them even for a simple forward iterator.

    Q: What is a const_iterator and do I need one?

    A const_iterator is an iterator whose operator* returns const T&, so you can read elements but not modify them — it is what you get from cbegin()/cend() or when iterating a const container. You need one whenever a container might be const (for (const auto& x : myConstBag)) or you want to guarantee read-only access. The usual approach is a single iterator template parameterised on const-ness, or a separate ConstIterator class; for a first custom iterator it is fine to add it later once the mutable version works.

    Mini-Challenge: a Countdown range

    No blanks this time — just a brief and an outline. Build a Countdown(int n) that range-based for can walk from n down to 1. Wire up the iterator yourself, run it, and check your output against the example in the comments.

    🎯 Mini-Challenge: build the Countdown iterator

    Write the iterator + begin()/end() so Countdown(5) prints 5 4 3 2 1.

    Try it Yourself »
    C++
    #include <iostream>
    using namespace std;
    
    // 🎯 MINI-CHALLENGE: a Countdown range
    //
    // Build a class Countdown(int n) that is iterable and yields the numbers
    // n, n-1, n-2, ... down to 1 (NOT 0), so range-based for can print them.
    //
    // Steps:
    // 1. Give Countdown a nested Iterator that stores the current value.
    // 2. operator*   -> return the current value.
    // 3. operator++  -> DECREASE the current value by 1, return *this.
    // 4. operator!=  -> "not finished" while current value differs from 
    ...

    🎉 Lesson Complete

    • ✅ An iterator needs operator* (read), operator++ (advance), and operator!= (test end)
    • ✅ The container needs begin() (first element) and end() (one past the last)
    • end() is a sentinel — &data[size] for arrays, nullptr for linked lists — never read
    • operator* returns T& to allow writing; return by value only for generated values
    • ✅ The five iterator_traits aliases (starting with iterator_category) unlock STL algorithms
    • ✅ A const_iterator (returning const T&) gives read-only iteration over const containers
    • Next lesson: Memory Pools — manage allocation yourself for speed and control

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service