C++ Iterators and Invalidation: Container Rules and Safe Erase Loops
Learn to read iterator ranges, match algorithms to categories, and mutate standard containers without using stale handles. Includes two fully traced erase examples.
KnowledgeGate Team
Exam prep & CS education

A C++ loop can compile, and its iterator can initially point to the expected value, yet an insert, erase, push_back, or rehash can silently make that position unusable. The symptom may be a skipped element, a wrong value, or undefined behaviour instead of a compiler error. The safe method is to mark the range, check the operations required, apply the exact container rule, and continue from the iterator returned by erase. The wider Coding & DSA catalogue provides more programming practice.
Related reading: Vector growth and capacity and STL algorithms.
C++ begin() and end(): read the half-open range first
A standard iterator range is written [first, last). first denotes the first element when the range is non-empty. last is the boundary one position past the final element, so it must never be dereferenced.
For std::vector<int> v{4, 7, 9, 12}, *v.begin() = 4, *(v.begin() + 2) = 9, and std::distance(v.begin(), v.end()) = 4. An ordinary loop visits 4, 7, 9, and 12, then increments once more until its iterator equals v.end(). It stops before another dereference. The sum is 4 + 7 + 9 + 12 = 32. For an empty vector, begin() == end(), so the body runs zero times. cbegin() and cend() express read-only traversal.
The iterator is the position-like object, while *it accesses its value. it + 2 works only for random-access iterators. Iterator categories determine which operations an iterator supports and which algorithms can use it. C++ Iterators Tutorial with Examples provides the broad traversal-and-algorithm foundation, including the baseline erase pattern. Mutation-time reasoning then depends on three additional tools: a container-by-container handle matrix, explicit capacity and rehash checks, and a full erase-loop ledger.
C++ iterator categories: operations determine which algorithms fit
An iterator category states the operations an algorithm may use, not whether an iterator survives mutation.
Category | Minimum useful operations | Standard-library examples |
|---|---|---|
Input | Single-pass read and | Input stream iterator |
Output | Single-pass write and | Output stream iterator |
Forward | Multi-pass traversal and repeated |
|
Bidirectional | Forward operations plus |
|
Random access | Constant-time jumps, difference, and ordering |
|
Contiguous | Random access plus adjacent storage |
|
std::sort can order std::vector<int>{9, 1, 7, 3, 2, 8} as {1, 2, 3, 7, 8, 9} because vector iterators are random access. A std::list<int> with the same values must use list.sort() to get that order. std::distance can subtract vector endpoints but advances through all six list elements. Category controls movement and cost; invalidation controls whether a particular iterator remains usable.
C++ iterator invalidation rules: name the container and mutation
Identify the container and operation. Here, handles means iterators, pointers, and references.
Container |
|
|
|
|---|---|---|---|
| Reallocation: all handles; otherwise, iterators and references at or after the point | Erased position and all iterators or references at or after it | Reallocation invalidates all handles |
| Existing handles remain valid | Only erased handles | Not applicable |
| Existing handles remain valid | Only erased handles | Not applicable |
| Iterators only if rehash occurs; references and pointers survive | Only erased handles | All iterators; retained references and pointers survive |
Start with v = {10, 20, 30, 40} and save old30 = v.begin() + 2. Calling next = v.erase(v.begin() + 1) removes 20 and gives {10, 30, 40}. next is valid at new index 1, with *next = 30. old30 is invalid because it followed the erased position, even if its old address looks plausible.
A rehash changes bucket arrangement, so unordered iterators are lost while retained references survive. Hashing and Collision Resolution explains the model.
![Before-and-after trace of erasing 20 from vector [10,20,30,40], leaving [10,30,40] with old30 invalidated and next pointing at 30.](https://cdn.knowledgegate.ai/blog-assets/blog_asset_1784644492041_edeul9.jpg)
Erase while iterating in C++: use the returned position
Use the iterator returned by erase instead of incrementing a stale one:
std::vector<int> values{3, 4, 6, 7, 8, 10};
for (auto it = values.begin(); it != values.end(); ) {
if (*it % 2 == 0) {
it = values.erase(it);
} else {
++it;
}
}Current value | Action | Sequence after action | New iterator target |
|---|---|---|---|
| Keep and increment |
|
|
| Erase |
|
|
| Erase |
|
|
| Keep and increment |
|
|
| Erase |
|
|
| Erase |
|
|
There is no unconditional header increment. erase supplies the next valid position; only the keeping branch advances explicitly. This avoids both stale access and skipped successors. Four values are removed, leaving size 2 and checksum 3 + 7 = 10. This shape works for containers whose member erase(it) returns the following iterator, including list and map. forward_list instead uses a predecessor with erase_after.
![Six-frame trace of a safe erase loop over vector [3,4,6,7,8,10] removing even values down to [3,7], with four removed and checksum 10.](https://cdn.knowledgegate.ai/blog-assets/blog_asset_1784644493028_2oaf32.jpg)
Safer C++ mutation patterns: reserve, erase_if, and reacquire
When only the filtered result matters, C++20 provides a whole-container operation:
std::erase_if(values, [](int x) { return x % 2 == 0; });For {3, 4, 6, 7, 8, 10}, it returns removal count 4 and leaves {3, 7}.
Capacity planning can protect iterators captured afterwards. Start with std::vector<int> v{10, 20, 30};, record old_capacity = v.capacity(), call v.reserve(old_capacity + 5), and only then capture it20 = v.begin() + 1. A subsequent v.push_back(40) cannot exceed the requested capacity, so it20 remains valid and denotes 20. The old past-the-end iterator does not remain valid. Any iterator captured before a reallocating reserve is lost.
Two robust designs are to retain a stable key or meaningful index and reacquire after mutation, or to separate selection from mutation into two phases. For unordered containers, reserve before retaining iterators. A later rehash can still invalidate them.
C++ invalidation traps: skipped values, stale end(), and plausible addresses
Code smell | What goes wrong | Correction |
|---|---|---|
Extra | Returned element skipped | Increment only without erase |
| Mutation invalidates affected position | Assign the returned iterator |
Cached vector | Boundary stale | Re-evaluate |
Dereferencing | Boundary is not an element | Stop before dereference |
Vector handles across possible reallocation | All become invalid | Reserve first or reacquire |
Unordered iterator after rehash | Iterator fails; retained references survive | Track types separately |
From {2, 4, 6}, erase 2 returns 4. An extra increment jumps to 6, skipping 4. The correct loop tests 4 and 6, finishing empty.
Name the container, mutation, and position; check capacity or buckets; mark affected handles and cached end(); then trace valid handles. Debug modes and sanitizers help but cannot legalise undefined behaviour.
C++ iterator questions: trace validity before tracing values
Three useful assessment patterns are: identify the minimum category an algorithm requires, decide which handles survive a stated mutation, and repair an erase loop with its exact output. If a stale iterator makes the program undefined, “what does it print?” has no dependable value to guess.
Check A: Start with vector {1, 2, 3, 4, 5}, save an iterator to 4, then erase 2. The result is {1, 3, 4, 5}. The returned iterator points to 3, while the saved iterator to 4 is invalid because it followed the erased position.
Check B: Start with list {1, 2, 3}, save an iterator to 2, then insert 9 immediately before it. The result is {1, 9, 2, 3}, and the saved iterator remains valid at 2.
General vector construction, access, sorting, and resizing belong in Vector in C++: Create, Iterate and Sort with Examples. The narrow question after a mutation is which saved iterator, pointer, reference, or past-the-end boundary remains usable.
C++ iterators and invalidation: the short version and next step
Use four steps: mark [begin, end), identify the required iterator category, apply the exact mutation rule for that container, and continue from the iterator returned by erase. The main trace is {3, 4, 6, 7, 8, 10} -> {3, 7}, with removed = 4 and checksum 10.
Now carefully reproduce both traces: erase 20 from {10, 20, 30, 40} and cross out old30, then filter the six-value vector without skipping. For broader language and STL study, continue with C++ Programming.
Keep learning

C++ unordered_map and unordered_set: Hashing, Equality and a Custom-Key Frequency Counter
Build a correct mental model for C++ unordered containers, then trace a custom-key counter through a real collision without merging distinct keys.

C++ STL Containers: Choose by Access Pattern, Mutation Cost and Ownership
Choose a C++ STL container from the workload, not from habit. Trace sequence operations, keyed counts, invalidation and object lifetime through exact examples.

C++ STL Algorithms: Sort, Search, Transform and Clean Data in One Pipeline
Trace nine sensor readings through sort, lower_bound, transform, erase-remove and accumulate, with every iterator rule and intermediate value explained.

C++ std::vector Internals: Growth, Capacity, Reallocation and Iterator Invalidation
Learn how std::vector manages contiguous storage, why an append can relocate every element, and which iterators, pointers and references survive each mutation.