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
- 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
| Piece | Lives on | Job | Returns |
|---|---|---|---|
| operator* | iterator | Read the current element | T& (or T by value) |
| operator++ | iterator | Move to the next element | *this by reference |
| operator!= | iterator | "Am I NOT at the end?" | bool |
| begin() | container | Cursor at the first element | an iterator |
| end() | container | Sentinel one past the last | an 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: 1552. 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? yes3. 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_iteratorFor 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
| Need | Code | Notes |
|---|---|---|
| Read current | int& operator*() const | return T& to allow writing |
| Advance | It& operator++() | return *this by reference |
| Not finished? | bool operator!=(const It& o) | compare cursors |
| First element | It begin() | cursor to data[0] / head |
| One past last | It end() | &data[size] or nullptr |
| STL category | using 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
- ✅ 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
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
- Previous: C++ Unit Testing (GoogleTest, Catch2, Doctest)
- Next: Memory Pools, Arenas & Custom Memory Management Techniques — Build arena and pool allocators for fast, predictable memory performance
- Quick reference: C++ cheat sheet
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.