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
++ and ==. You should also know the difference between returning by value and by reference. Everything else we'll build here.💡 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.
Worked example: an iterable IntBag
Read every comment, run it, and check the output matches.
#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.
#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.
#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.
#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_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: writeoperator!=for the loop, thenbool operator==(const It& o) const { return !(*this != o); }for free. - 💡 Return
*thisfromoperator++: prefix increment must return the iterator by reference so the loop keeps stepping. - 💡 Match
begin()andend()types: they must return the same iterator type, orit != 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 usenullptr. - Off-by-one in the loop: the rule is half-open
[begin, end)—end()is never read. Using<=-style logic (stopping only after readingend) reads one element too many. Compare with!=against the one-past sentinel. - Missing
operator!=: "error: no match for 'operator!='" — range-basedforcan't compile without it. Addbool operator!=(const It& o) const(the loop calls it every step to test againstend()). - Returning by value when you meant by reference:
int operator*() consthands back a copy, sofor (auto& x : c) x = 9;changes nothing. Returnint&if callers must write through it; return by value only for generated values that aren't stored. - "error: no member named 'begin'": range-based
forneedsbegin()andend()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. |
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.
#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), andoperator!=(test end) - ✅ The container needs
begin()(first element) andend()(one past the last) - ✅
end()is a sentinel —&data[size]for arrays,nullptrfor linked lists — never read - ✅
operator*returnsT&to allow writing; return by value only for generated values - ✅ The five
iterator_traitsaliases (starting withiterator_category) unlock STL algorithms - ✅ A
const_iterator(returningconst T&) gives read-only iteration overconstcontainers - ✅ 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.