Traverse Any Container with C++ Iterators
Learn to use C++ iterators to walk, search, and modify any standard container uniformly, and understand the five iterator categories that power the STL.
By the end of this page, you will understand what iterators are and why C++ needs them, write iterator-based loops over standard containers, recognise the five iterator categories and what each one supports, and avoid the three most common iterator bugs that cause undefined behaviour.
What and Why
Imagine you have three boxes of data: a std::vector (items in a contiguous array), a std::list (items linked by pointers), and a std::set (items in a balanced tree). Each stores data completely differently internally. If you wanted to write a generic print_all function, you would normally need a separate version for each container type.
Iterators solve this problem. An iterator is an object that represents a position inside a sequence. It exposes a small, consistent interface β dereference to get the current value, increment to advance, compare to know when you have reached the end β regardless of how the underlying container is laid out in memory.
The result is that a single algorithm, say std::sort, works on any container that provides the right kind of iterators. You write the algorithm once; the container supplies the navigation.
Think of an iterator the way you would think of a cursor in a text editor. The cursor knows where it is in the document, you can move it forward or backward, and you can read or replace the character it currently points at. The cursor does not need to understand whether the document is stored as one long string or as a tree of paragraphs.
Step by Step
The raw iterator loop (C++11)
Every standard container provides begin() and end() member functions. begin() returns an iterator positioned at the first element; end() returns one past the last element β a sentinel that marks "you have gone too far."
#include <iostream>
#include <vector>
int main() {
std::vector<int> numbers = {10, 20, 30, 40, 50};
// it starts at the first element; the loop runs while it != end()
for (auto it = numbers.begin(); it != numbers.end(); ++it) {
std::cout << *it << '\n'; // dereference to get the value
}
}Three operations cover almost all iterator use:
| Expression | Meaning |
|---|---|
*it | Read (or write) the value at the current position |
++it | Advance to the next position |
it != end() | Check whether you have passed the last element |
Range-based for is iterator sugar (C++11)
The range-based for loop you already know is exactly the raw loop above, written by the compiler on your behalf:
#include <iostream>
#include <vector>
int main() {
std::vector<int> numbers = {10, 20, 30, 40, 50};
// The compiler expands this into the begin/end loop above.
for (int n : numbers) {
std::cout << n << '\n';
}
}Understanding that range-for uses iterators matters when the loop body must modify elements or when you need the iterator itself (for example, to erase during traversal).
Reading without modifying: const_iterator
When you only read from a container, prefer cbegin() and cend(). They return const_iterator objects that refuse to let you modify the underlying element, catching accidental mutations at compile time.
#include <iostream>
#include <vector>
void print(const std::vector<int>& v) {
for (auto it = v.cbegin(); it != v.cend(); ++it) {
// *it = 0; // compile error β iterator is read-only
std::cout << *it << '\n';
}
}
int main() {
std::vector<int> numbers = {1, 2, 3};
print(numbers);
}The five iterator categories
Not every iterator can do everything. C++ defines a hierarchy of categories based on what operations are supported. The category a container provides determines which algorithms can work with it.
| Category | Supports | Example containers |
|---|---|---|
| Input | Single-pass read-only (*it, ++it) | std::istream_iterator |
| Output | Single-pass write-only (*it =, ++it) | std::ostream_iterator |
| Forward | Multi-pass read/write, one direction | std::forward_list |
| Bidirectional | Forward + --it | std::list, std::map |
| Random-access | Bidirectional + it + n, it[n] | std::vector, std::deque |
std::sort requires random-access iterators (it needs it + n for its partition steps). std::find only needs input iterators. When a call to std::sort fails to compile, mismatched iterator categories are often the reason.
Common Patterns
Pattern 1 β finding and erasing a single element
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {3, 1, 4, 1, 5, 9};
auto it = std::find(v.begin(), v.end(), 4);
if (it != v.end()) {
v.erase(it); // erase the element at the found position
}
for (int n : v) std::cout << n << ' '; // 3 1 1 5 9
}Always check it != v.end() before using a result from std::find. If the value is absent, find returns end(), and dereferencing it is undefined behaviour.
Pattern 2 β iterating a sub-range
Because iterators are just positions, you can pass any valid range [first, last) to an algorithm β the range does not have to cover the whole container:
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {5, 3, 8, 1, 9, 2, 7};
// Sort only the first four elements; leave the rest untouched.
std::sort(v.begin(), v.begin() + 4);
for (int n : v) std::cout << n << ' '; // 1 3 5 8 9 2 7
}Pattern 3 β reverse iteration
Every bidirectional container provides rbegin() and rend() for walking backwards without reversing the container itself:
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
for (auto it = v.rbegin(); it != v.rend(); ++it) {
std::cout << *it << ' '; // 5 4 3 2 1
}
}What Can Go Wrong
Dereferencing end()
end() points one past the last element β there is nothing there. Dereferencing it is undefined behaviour and typically causes a crash or silent data corruption.
std::vector<int> v = {1, 2, 3};
auto it = std::find(v.begin(), v.end(), 99); // not found
// BAD β it == v.end(), so *it is undefined behaviour
std::cout << *it;
// GOOD β always guard the result
if (it != v.end()) {
std::cout << *it;
}Iterator invalidation
Many container operations invalidate existing iterators β the iterator no longer points at a valid location. For std::vector, any insertion that causes a reallocation invalidates all iterators into that vector. Erasing an element invalidates iterators at and after the erased position.
std::vector<int> v = {1, 2, 3, 4, 5};
auto it = v.begin() + 2; // points at 3
v.push_back(6); // may reallocate β it is now invalid!
std::cout << *it; // undefined behaviour
// GOOD β capture the index instead, or re-obtain the iterator after modificationErasing inside a loop without updating the iterator
// BAD β incrementing an invalidated iterator is undefined behaviour
for (auto it = v.begin(); it != v.end(); ++it) {
if (*it % 2 == 0) v.erase(it); // erase returns the next valid iterator, but we ignore it
}
// GOOD β erase returns the next valid iterator; assign it back
for (auto it = v.begin(); it != v.end(); ) {
if (*it % 2 == 0) it = v.erase(it);
else ++it;
}Quick Reference
| Task | Code |
|---|---|
| Loop over all elements | for (auto it = c.begin(); it != c.end(); ++it) |
| Read-only loop | for (auto it = c.cbegin(); it != c.cend(); ++it) |
| Reverse loop | for (auto it = c.rbegin(); it != c.rend(); ++it) |
| Find a value | auto it = std::find(first, last, value); |
| Check find succeeded | if (it != c.end()) { ... } |
| Erase safely in loop | it = c.erase(it); then skip ++it |
| Distance between iterators | std::distance(first, last) |
| Advance N steps | std::advance(it, n) (or it + n for random-access) |
What's Next
- STL Algorithms β
std::sort,std::transform,std::copy, and forty more algorithms that accept iterator pairs. - Containers in Depth β understand which iterator category each container provides and why that shapes which algorithms you can use.
- Const Correctness β see how
const_iteratorfits into C++'s broader const model. - C++20 introduces ranges, a higher-level layer built on top of iterators that removes many iterator-pair boilerplate patterns. Once you are comfortable with iterators, ranges are the natural next step.