Skip to content
C++
Language
since C++11
Basic

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."

cpp
#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:

ExpressionMeaning
*itRead (or write) the value at the current position
++itAdvance 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:

cpp
#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.

cpp
#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.

CategorySupportsExample containers
InputSingle-pass read-only (*it, ++it)std::istream_iterator
OutputSingle-pass write-only (*it =, ++it)std::ostream_iterator
ForwardMulti-pass read/write, one directionstd::forward_list
BidirectionalForward + --itstd::list, std::map
Random-accessBidirectional + 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

cpp
#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:

cpp
#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:

cpp
#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.

cpp
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.

cpp
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 modification

Erasing inside a loop without updating the iterator

cpp
// 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

TaskCode
Loop over all elementsfor (auto it = c.begin(); it != c.end(); ++it)
Read-only loopfor (auto it = c.cbegin(); it != c.cend(); ++it)
Reverse loopfor (auto it = c.rbegin(); it != c.rend(); ++it)
Find a valueauto it = std::find(first, last, value);
Check find succeededif (it != c.end()) { ... }
Erase safely in loopit = c.erase(it); then skip ++it
Distance between iteratorsstd::distance(first, last)
Advance N stepsstd::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_iterator fits 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.