C++ Iterators Tutorial with Examples: Traversal, Algorithms and Invalidation

Learn how C++ iterators connect containers to algorithms. Trace vector, list and map examples, then practise safe erasure and invalidation fixes.

KnowledgeGate Team

Exam prep & CS education

Updated 5 Aug 20266 min read

You see auto it = values.begin() and it is tempting to call an iterator a pointer with a longer name. An iterator actually joins a container to an algorithm. Its operations and lifetime depend on the category and the container, which is why begin() + 2 compiles on a vector and is rejected on a list, and why an iterator held across an erase call can silently become invalid.

What an Iterator Represents in C++

An iterator identifies a position in a sequence and supports operations appropriate to its category. Most STL algorithms use a half-open range [first, last): first is included, last is excluded. end() is the one-past-the-last sentinel, so never dereference it.

std::vector<int> values{12, 5, 18, 7, 9};
auto it = values.begin();       // *it is 12
++it;                           // *it is 5

for (auto pos = values.begin(); pos != values.end(); ++pos)
    std::cout << *pos << ' ';   // 12 5 18 7 9

From the second element, four more increments make it == values.end(). The loop checks before dereferencing. A regular iterator permits *it = 20; auto it = std::as_const(values).begin() gives a movable, readable const_iterator that cannot assign to the element. For structured coverage of STL, concepts, MCQs and coding questions, use the C++ Programming course.

A five-cell vector holding 12, 5, 18, 7, 9, with begin() at index 0 and end() marking the position one past the last cell.

Category

Defining ability

Familiar example

Input

Single-pass read

Input stream iterator

Output

Single-pass write

Output stream iterator

Forward

Repeatable forward movement

std::forward_list

Bidirectional

Forward movement and --it

std::list, std::map

Random-access

it + n, subtraction, ordering

std::deque

Contiguous

Random access plus contiguous storage

std::vector, std::array, raw pointers

Generic code should use std::next, std::advance and std::distance instead of assuming it + n exists:

std::list<int> nums{40, 10, 30, 20};
auto it = nums.begin();
std::advance(it, 2);            // *it is 30

nums.begin() + 2 does not compile. Advancing a list iterator by two takes two increments; a random-access iterator can jump directly.

Fully Worked Vector Example: Traverse, Find and Modify

This complete program doubles odd elements, then searches the result.

#include <algorithm>
#include <iostream>
#include <numeric>
#include <vector>

int main() {
    std::vector<int> values{12, 5, 18, 7, 9};

    for (auto it = values.begin(); it != values.end(); ++it) {
        if (*it % 2 != 0) *it *= 2;
    }

    std::cout << "after:";
    for (auto it = values.begin(); it != values.end(); ++it)
        std::cout << ' ' << *it;
    std::cout << "\nsum: "
              << std::accumulate(values.begin(), values.end(), 0) << '\n';

    auto found = std::find(values.begin(), values.end(), 14);
    if (found != values.end())
        std::cout << "14 found at index "
                  << std::distance(values.begin(), found) << '\n';

    auto missing = std::find(values.begin(), values.end(), 99);
    if (missing == values.end()) std::cout << "99 not found\n";
}

The trace is 12 stays 12, 5 becomes 10, 18 stays 18, 7 becomes 14, and 9 becomes 18. Thus the vector is {12, 10, 18, 14, 18}, and 12 + 10 + 18 + 14 + 18 = 72. The output is:

after: 12 10 18 14 18
sum: 72
14 found at index 3
99 not found

The missing search returns end() and must not be dereferenced. Mutation is safe because neither vector size nor storage changes.

Iterators Turn Container Positions into Algorithm Ranges

Reset values to {12, 5, 18, 7, 9}. The call std::sort(values.begin() + 1, values.begin() + 4) selects indices 1, 2, 3, containing {5, 18, 7}. It excludes index 4, so the result is {12, 5, 7, 18, 9}. See the sorting algorithms comparison for the underlying methods.

Now std::accumulate(values.begin(), values.end(), 0) gives 12 + 5 + 7 + 18 + 9 = 51. A std::count_if predicate testing x % 2 != 0 returns 3, for 5, 7 and 9. Algorithms receive iterator positions, not the container itself.

std::sort requires random-access iterators. It works with vector, but not list; std::list<int>{40, 10, 30, 20}.sort() instead produces {10, 20, 30, 40}.

Reading List and Map Iterators Correctly

Traversing std::list<int> nums{40, 10, 30, 20} from begin() to end() prints 40 10 30 20. Its bidirectional iterator can move back from the sentinel:

auto it = nums.end();
--it;                           // *it is 20

For std::map<std::string, int> scores{{"Asha", 81}, {"Kabir", 67}, {"Meera", 92}}, for (auto it = scores.begin(); it != scores.end(); ++it) prints Asha:81 Kabir:67 Meera:92 in key order. it->first is the unchangeable key; it->second is the mapped score, so Kabir's 67 may become 70.

The read-only equivalent is for (const auto& [name, score] : scores). Range-based loops use iterators underneath, but explicit iterators are needed when the position matters or erase returns the next position.

Iterator Invalidation and the Safe Erase Pattern

Vector reallocation invalidates all iterators. Without reallocation, vector insertion or erasure invalidates iterators at and after the affected position. Erasing one list or map element invalidates only iterators to that element. Check the specific container rules whenever an iterator survives a structural change.

std::vector<int> values{10, 20, 30, 40};
auto it = values.begin() + 1;   // *it is 20
it = values.erase(it);          // values is {10, 30, 40}; *it is 30

Never reuse the old iterator after erasure. Use the returned position:

std::vector<int> values{3, 4, 6, 7, 8};
for (auto it = values.begin(); it != values.end(); ) {
    if (*it % 2 == 0) it = values.erase(it);
    else ++it;
}
// values is {3, 7}

The loop keeps 3, erases 4, erases 6 at the returned position, keeps 7, then erases 8. An unconditional ++it after erase would skip the element that moved into the erased position. Erasing structurally inside a range-based loop is unsafe because its hidden iterator is not repaired.

Erasing the value 20 from the vector {10, 20, 30, 40} leaves {10, 30, 40}, invalidates the old iterators and returns the new position, beside a trace panel reducing {3, 4, 6, 7, 8} to {3, 7}.

Common Iterator Errors and How to Repair Them

  • Dereferencing end() is invalid. Check it != end() first.

  • list.begin() + 2 is illegal. Use std::next(list.begin(), 2).

  • A vector push_back may reallocate. Reacquire the iterator, or retain an index when appropriate.

  • Iterators from different containers are not comparable. Take both boundaries from the same container.

Replace for (auto it = v.begin(); it <= v.end(); ++it) with it != v.end(). Relational comparison such as <= exists only for random-access and contiguous iterators, and even on a vector the faulty loop runs its body one extra time with it equal to end(), dereferencing the sentinel.

When debugging, identify the container and iterator category, mark every size or capacity change, validate the iterator before dereferencing, and confirm the algorithm's iterator requirement.

How Iterators Are Tested, Plus Three Practice Tasks

Exam and interview questions on iterators ask you to predict loop output, identify undefined behaviour, match a container to a category, calculate std::distance, repair an erase loop, or decide whether an algorithm accepts an iterator. For placement preparation, connect these mechanics to a practical coding-round strategy.

  1. Copy std::list<int>{2, 4, 6, 8} through rbegin() and rend() into a vector. Expected: {8, 6, 4, 2}.

  2. Safely erase negatives from std::vector<int>{4, -1, 0, -3, 7}. Expected: {4, 0, 7}.

  3. Apply std::max_element to std::map<std::string, int>{{"Ravi", 68}, {"Neha", 91}, {"Iqbal", 84}} with a comparator on .second. Expected: Neha:91.

The Short Version

Iterators define half-open ranges, their category controls legal movement, and container mutations control validity. Explore more problems in the live Coding and DSA category. Use the C++ Programming course for the language and STL path, then Coding For Placements to apply them in coding rounds.

Compile the three exercises. Then deliberately trigger one invalidation bug and repair it with the correct returned iterator.