Custom Iterators

Reviewed & published by Brayan K

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().

Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

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

#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 {
        int* ptr_;                // where the cursor currently points
    public:
        Iterator(int* p) : ptr_(p) {}

        // operator*  -> read the value the cursor points at.
        // Return BY REFERENCE (int&) so callers can also WRITE through it.
        int& operator*() const { return *ptr_; }

        // operator++ -> step the cursor to the next slot. Pre-increment
        // returns *this BY REFERENCE so the loop can keep advancing.
        Iterator& operator++() { ++ptr_; return *this; }

        // operator!= -> "are we NOT finished yet?". The for-loop calls this
        // every step to compare against end(). Without it, no loop.
        bool operator!=(const Iterator& o) const { return ptr_ != o.ptr_; }
        bool operator==(const Iterator& o) const { return ptr_ == o.ptr_; }
    };

    // begin() -> cursor at the FIRST element.
    Iterator begin() { return Iterator(&data_[0]); }
    // end()   -> cursor ONE PAST the last element (the sentinel / "stop here").
    Iterator end()   { return Iterator(&data_[5]); }
};

int main() {
    IntBag bag(10, 20, 30, 40, 50);

    // Range-based for is just sugar for:
    //   for (auto it = bag.begin(); it != bag.end(); ++it) { int x = *it; ... }
    cout << "Contents: ";
    for (int x : bag) cout << x << " ";     // Contents: 10 20 30 40 50
    cout << endl;

    // Because operator* returns int&, we can WRITE through the iterator:
    for (int& x : bag) x += 1;              // bump every value by 1
    cout << "Bumped:   ";
    for (int x : bag) cout << x << " ";     // Bumped:   11 21 31 41 51
    cout << endl;

    // The same begin()/end() let us total it up by hand:
    int sum = 0;
    for (int x : bag) sum += x;
    cout << "Sum:      " << sum << endl;    // Sum:      155

    return 0;
}

// ✅ Expected output:
//    Contents: 10 20 30 40 50
//    Bumped:   11 21 31 41 51
//    Sum:      155

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:

#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, written, and passed over more than once.
        using iterator_category = forward_iterator_tag;
        using value_type        = int;
        using difference_type   = ptrdiff_t;
        using pointer           = int*;
        using reference         = int&;

        Iterator(int* p) : ptr_(p) {}
        reference operator*() const { return *ptr_; }
        Iterator& operator++() { ++ptr_; return *this; }
        bool operator!=(const Iterator& o) const { return ptr_ != o.ptr_; }
        bool operator==(const Iterator& o) const { return ptr_ == o.ptr_; }
    };

    Iterator begin() { return Iterator(&data_[0]); }
    Iterator end()   { return Iterator(&data_[5]); }
};

int main() {
    IntBag bag(3, 9, 1, 7, 4);

    // Now standard algorithms accept bag.begin()/bag.end() directly:
    int total = accumulate(bag.begin(), bag.end(), 0);
    cout << "Total:      " << total << endl;          // Total:      24

    auto biggest = max_element(bag.begin(), bag.end());
    cout << "Biggest:    " << *biggest << endl;       // Biggest:    9

    int evens = count_if(bag.begin(), bag.end(),
                         [](int x) { return x % 2 == 0; });
    cout << "Evens:      " << evens << endl;          // Evens:      1

    auto found = find(bag.begin(), bag.end(), 7);
    cout << "Found 7?    " << (found != bag.end() ? "yes" : "no") << endl;

    return 0;
}

// ✅ Expected output:
//    Total:      24
//    Biggest:    9
//    Evens:      1
//    Found 7?    yes

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.

#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 the cursor points at (read one word)
        const char* operator*() const { return ___; }   // 👉 *ptr_

        // 2) Advance the cursor to the next slot, return *this
        Iterator& operator++() { ___; return *this; }    // 👉 ++ptr_

        // 3) "Not finished?" — compare the two cursors
        bool operator!=(const Iterator& o) const { return ___; }  // 👉 ptr_ != o.ptr_
    };

    Iterator begin() { return Iterator(&words_[0]); }
    Iterator end()   { return Iterator(&words_[3]); }   // one past the last
};

int main() {
    WordList list("learn", "coding", "fast");
    for (const char* w : list) cout << w << " ";

    // ✅ Expected output:
    //    learn coding fast
    return 0;
}

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.

#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 to the next node in the chain
        Iterator& operator++() { cur_ = ___; return *this; }   // 👉 cur_->next
        bool operator!=(const Iterator& o) const { return cur_ != o.cur_; }
    };

    // 4) begin() points at the first node; end() is the "past the end"
    //    sentinel. For a linked list, that sentinel is nullptr.
    Iterator begin() { return Iterator(head_); }
    Iterator end()   { return Iterator(___); }   // 👉 nullptr
};

int main() {
    IntList nums;
    nums.pushFront(3);   // list is now: 3
    nums.pushFront(2);   // list is now: 2 -> 3
    nums.pushFront(1);   // list is now: 1 -> 2 -> 3

    int sum = 0;
    for (int x : nums) { cout << x << " "; sum += x; }
    cout << "= " << sum << endl;

    // ✅ Expected output:
    //    1 2 3 = 6
    return 0;
}

🔎 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

Common Errors (and the fix)

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

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.

#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 end's.
// 5. begin() -> Iterator(n);   end() -> Iterator(0);
//    (end is the sentinel one past the last value you want, i.e. 0).
//
// ✅ Expected output for Countdown(5):
//    5 4 3 2 1

int main() {
    // Countdown c(5);
    // for (int x : c) cout << x << " ";
    // cout << endl;

    // your code here
    return 0;
}

🎉 Lesson Complete

Practice quiz

Which three iterator operators (plus begin()/end()) are the minimum needed for range-based for?

  • operator+, operator-, operator[]
  • operator==, operator=, operator<
  • operator* (read), operator++ (advance), operator!= (test end)
  • begin, end, size

Answer: operator* (read), operator++ (advance), operator!= (test end). range-based for needs operator*, prefix operator++, and operator!=, with begin()/end() on the container.

Why does end() point ONE PAST the last element rather than at it?

  • It's the half-open range [begin, end) convention; the loop stops at the sentinel without dereferencing it
  • To save memory
  • So end() can be read safely
  • It points at the last element actually

Answer: It's the half-open range [begin, end) convention; the loop stops at the sentinel without dereferencing it. The half-open range makes the loop condition simply it != end(), handles empty containers naturally, and end() is never dereferenced.

A range-based 'for (auto x : c)' is rewritten by the compiler to roughly:

  • for (int i = 0; i < c.size(); i++)
  • while (c.next())
  • c.forEach(...)
  • for (auto it = c.begin(); it != c.end(); ++it) { auto x = *it; ... }

Answer: for (auto it = c.begin(); it != c.end(); ++it) { auto x = *it; ... }. It's pure sugar over begin()/end(), ++it, and *it each step.

When should operator* return by reference (T&) rather than by value?

  • Always by value
  • When callers should be able to read AND write the stored element through the iterator
  • Only for const iterators
  • Never

Answer: When callers should be able to read AND write the stored element through the iterator. Returning T& lets 'for (auto& x : c) x = ...;' modify the container; return by value only for generated values that aren't stored.

For a linked list, what should end() return as its sentinel?

  • nullptr
  • The head node
  • &data[size]
  • The last node

Answer: nullptr. A linked-list iterator hops node to node; the 'one past the last' sentinel is nullptr.

What must prefix operator++ return so the loop keeps advancing?

  • A copy of the iterator
  • void
  • *this by reference
  • the new value

Answer: *this by reference. Prefix increment returns *this by reference so the loop can keep stepping the same iterator.

What does std::iterator_traits read from your iterator?

  • The container's size
  • Five type aliases: iterator_category, value_type, difference_type, pointer, reference
  • The begin and end functions
  • The element values

Answer: Five type aliases: iterator_category, value_type, difference_type, pointer, reference. Those five aliases tell STL algorithms what kind of iterator it is and how it behaves.

Which iterator category does std::sort require?

  • input iterator
  • forward iterator
  • bidirectional iterator
  • random-access iterator

Answer: random-access iterator. sort needs random access (+ n and []); std::find, by contrast, only needs a forward iterator.

What is a const_iterator?

  • An iterator that cannot move
  • An iterator whose operator* returns const T&, allowing read-only access
  • An iterator for constexpr only
  • The same as end()

Answer: An iterator whose operator* returns const T&, allowing read-only access. A const_iterator (from cbegin()/cend() or a const container) returns const T& so elements can't be modified.

If operator* returns by value (int) instead of by reference, what happens to 'for (auto& x : c) x = 9;'?

  • It modifies the container
  • It fails to compile always
  • It changes nothing — it writes to a temporary copy
  • It deletes elements

Answer: It changes nothing — it writes to a temporary copy. Returning a copy means writes go to a temporary; return int& if callers must write through the iterator.

Continue this course

Frequently asked questions

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.

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

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.

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.

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.

Related lessons