C++ Iterators
- Description: A note on iterator categories,
std::iterator_traits, common iterator operations, iterator adaptors, writing custom iterators, C++20 iterator concepts, and iterator invalidation - My Notion Note ID: K2A-B1-12
- Created: 2018-10-10
- Updated: 2026-02-28
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. Iterator Categories
- 2.
std::iterator_traits - 3. Common Iterator Operations
- 4. Iterator Adaptors
- 5. Writing a Custom Iterator
- 6. Iterator Concepts (C++20)
- 7. Iterator Invalidation
1. Iterator Categories
Iterators are classified by what operations they support. Each category subsumes all operations of weaker categories.
| Category | What you can do | Example |
|---|---|---|
| Input | Read once, advance forward | std::istream_iterator |
| Output | Write once, advance forward | std::ostream_iterator, std::back_inserter |
| Forward | Read/write, advance forward, multi-pass | std::forward_list::iterator |
| Bidirectional | + go backward | std::list, std::map, std::set iterators |
| Random access | + jump by N, compare with <, +/- |
std::deque iterators |
| Contiguous (C++20) | Random access + addresses are contiguous in memory | std::vector, std::array, raw arrays |
The hierarchy:
Input ──────► Forward ──► Bidirectional ──► Random access ──► Contiguous
Output ▲
│
std::vector iterator
A function template can require a particular category:
template <typename It>
void example(It first, It last) {
// For input iterators: only one pass through [first, last)
// For random access: can use first[i], first + n, last - first
}
Algorithms that need backward iteration (std::reverse) require bidirectional; ones that need O(1) distance computation (std::sort) require random access.
2. std::iterator_traits
std::iterator_traits<It> (<iterator>) exposes type metadata about an iterator:
#include <iterator>
using It = std::vector<int>::iterator;
std::iterator_traits<It>::value_type; // int
std::iterator_traits<It>::reference; // int&
std::iterator_traits<It>::pointer; // int*
std::iterator_traits<It>::difference_type; // ptrdiff_t (for it1 - it2)
std::iterator_traits<It>::iterator_category; // std::random_access_iterator_tag
Generic algorithms use iterator_traits to access these types without knowing the concrete iterator type:
template <typename It>
typename std::iterator_traits<It>::value_type
sum(It first, It last) {
typename std::iterator_traits<It>::value_type total{};
for (; first != last; ++first) total += *first;
return total;
}
In C++20+, concepts (std::input_iterator, etc.) make these requirements much cleaner — see § 6.
3. Common Iterator Operations
#include <iterator>
std::vector<int> v = {1, 2, 3, 4, 5};
auto it = v.begin();
++it; // advance one
*it; // dereference (returns reference to element)
it->member; // member access (for class types)
it1 == it2; // equality
it2 - it1; // distance (random-access only)
// Generic forms (work on any iterator category):
std::advance(it, 3); // advance by 3 (uses += for RA, loops for input)
auto next_it = std::next(it, 2); // = it + 2 for RA, multi-step for others
auto prev_it = std::prev(it); // bidirectional or RA
auto d = std::distance(it1, it2); // it2 - it1 for RA, count for others
// Insertion-style "iterators" don't really iterate; they consume writes
auto out = std::back_inserter(v);
*out++ = 99; // calls v.push_back(99)
std::next/std::prev/std::advance/std::distance work on any iterator category and pick the most efficient implementation based on iterator_category.
4. Iterator Adaptors
<iterator> provides adaptors that turn one kind of iterator into another.
| Adaptor | Effect |
|---|---|
std::back_inserter(c) |
Output iterator that calls c.push_back(x) |
std::front_inserter(c) |
c.push_front(x) |
std::inserter(c, pos) |
c.insert(pos, x) |
std::reverse_iterator |
Walks backward (vector::rbegin/rend) |
std::move_iterator |
Dereferences as rvalue (move instead of copy) |
std::istream_iterator<T>(stream) |
Reads Ts from a stream |
std::ostream_iterator<T>(stream, sep) |
Writes Ts with a separator |
// Copy from input stream into a vector
std::vector<int> v;
std::copy(std::istream_iterator<int>{std::cin},
std::istream_iterator<int>{}, // sentinel for EOF
std::back_inserter(v));
// Print a vector with a separator
std::copy(v.begin(), v.end(),
std::ostream_iterator<int>{std::cout, ", "});
// Move elements out of a source vector
std::vector<std::string> dst;
std::copy(std::make_move_iterator(src.begin()),
std::make_move_iterator(src.end()),
std::back_inserter(dst));
Adaptors let you reuse generic algorithms for I/O, container insertion, and reverse traversal without writing custom loops.
5. Writing a Custom Iterator
A minimal random-access iterator over a contiguous buffer:
#include <iterator>
#include <cstddef>
template <typename T>
class MyIter {
public:
using value_type = T;
using reference = T&;
using pointer = T*;
using difference_type = std::ptrdiff_t;
using iterator_category = std::random_access_iterator_tag;
MyIter(T* p) : p_(p) {}
reference operator*() const { return *p_; }
pointer operator->() const { return p_; }
MyIter& operator++() { ++p_; return *this; }
MyIter operator++(int) { auto t = *this; ++p_; return t; }
MyIter& operator--() { --p_; return *this; }
MyIter operator--(int) { auto t = *this; --p_; return t; }
MyIter& operator+=(difference_type n) { p_ += n; return *this; }
MyIter& operator-=(difference_type n) { p_ -= n; return *this; }
MyIter operator+ (difference_type n) const { return MyIter(p_ + n); }
MyIter operator- (difference_type n) const { return MyIter(p_ - n); }
difference_type operator-(MyIter o) const { return p_ - o.p_; }
reference operator[](difference_type n) const { return p_[n]; }
auto operator<=>(const MyIter&) const = default; // C++20: all comparisons
private:
T* p_;
};
In C++20, std::ranges::iterator_t<R> + concepts give you cleaner constraints when consuming custom iterators. For producing iterators in modern code, std::generator<T> (C++23 coroutines) is often easier than writing the iterator class by hand.
6. Iterator Concepts (C++20)
C++20 added named concepts that capture each iterator category. These give better error messages and cleaner template constraints.
| Concept | Replaces |
|---|---|
std::input_or_output_iterator |
Base of all iterators |
std::input_iterator |
Input category |
std::output_iterator<T> |
Output category writing T |
std::forward_iterator |
Forward category |
std::bidirectional_iterator |
Bidirectional category |
std::random_access_iterator |
Random-access category |
std::contiguous_iterator |
Contiguous category |
std::sentinel_for<I> |
Sentinel that terminates iteration of I |
template <std::random_access_iterator It>
void quicksort(It first, It last);
template <std::input_iterator It, std::sentinel_for<It> Sent>
auto count_until(It first, Sent last);
In C++20+ ranges, sentinels (the "end" position) need not be the same type as the iterator — std::ranges::range uses a (begin, sentinel) pair, allowing iterations like "until null terminator" or "until predicate" without dummy "past-the-end" iterators.
7. Iterator Invalidation
After certain container operations, existing iterators may no longer point to valid elements. The exact rules differ per container.
| Container | Invalidates |
|---|---|
std::vector, std::string |
All iterators on reallocation (e.g. push_back past capacity); iterators at-or-after the modification point on insert/erase |
std::deque |
All iterators on push_front or push_back; all on insert in the middle |
std::list, std::forward_list |
Only iterators to erased elements |
std::set, std::map (and their multi/unordered variants) |
Only iterators to erased elements |
std::unordered_* |
Iterators invalidated on rehash; references and pointers to elements remain valid |
The "erase-remove" idiom illustrates a common safe pattern:
// vector::erase invalidates iterators at-or-after the erase point.
// Use the returned iterator to continue safely:
for (auto it = v.begin(); it != v.end(); ) {
if (*it % 2 == 0) {
it = v.erase(it); // erase returns iterator to next element
} else {
++it;
}
}
For unordered_map / unordered_set, calling reserve(n) upfront can avoid rehashing, keeping iterators stable.
The general rule: don't hold an iterator across a container modification unless you've checked the container's invalidation rules.