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.