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() .
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free C++ course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
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.
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.
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.
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.
Add those aliases and the same begin() / end() now flow straight into the STL. Run this:
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.
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.
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.
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.
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.
Practice quiz
Which three iterator operators (plus begin()/end()) are the minimum needed for range-based for?
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
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.