Question
What are the iterator invalidation rules for C++ standard library containers?
In particular, when do operations such as insertion, erasure, resizing, reallocation, rehashing, and swapping make existing iterators, references, pointers, or past-the-end iterators unusable?
Short Answer
By the end of this page, you will know what iterator invalidation means, why using an invalid iterator is dangerous, and which common container operations invalidate iterators in C++. You will also learn safe patterns for erasing and inserting elements while iterating.
Concept
An iterator is an object that identifies an element in a container, much like a pointer identifies an object in memory. For example, std::vector<int>::iterator can point to an integer stored in a vector.
An iterator is invalidated when a container operation means that the iterator can no longer safely be used. Dereferencing, incrementing, comparing, or passing an invalid iterator to a container operation can cause undefined behavior.
Invalidation happens because many containers must move elements or rebuild internal storage:
- A
std::vectormay allocate a larger contiguous memory block and move every element there. - A
std::dequemay reorganize its internal blocks. - An
std::unordered_mapmay rehash its buckets. - Removing an element always invalidates iterators that refer to that removed element.
The important detail is that invalidation rules are defined per container and operation. Do not assume that a rule for std::list also applies to std::vector.
Also distinguish these related terms:
- Iterator validity: whether an iterator may still be used.
- Reference validity: whether
T& ref = container[i]still refers to the same element. - Pointer validity: whether
T* ptr = &container[i]still points to the same element. - Past-the-end iterator: the iterator returned by
end(). It does not refer to an element and often becomes invalid after size-changing operations.
Mental Model
Think of a container as a collection of numbered seats.
An iterator is a ticket that says, “I am sitting in this specific seat.” If the venue moves everyone to a new building, your old ticket no longer identifies a usable seat. This is similar to a std::vector reallocation.
If one person leaves a linked-list-style venue, everyone else's seats stay where they are. Only the ticket for the person who left becomes invalid. This is similar to std::list::erase.
Before using an old iterator after changing a container, ask: Did this operation move, remove, or reorganize the element or storage my iterator depends on? If the answer may be yes, obtain a fresh iterator.
Syntax and Examples
Iterators are commonly obtained with begin(), end(), or a search operation:
#include <vector>
std::vector<int> values{10, 20, 30};
auto it = values.begin(); // Refers to 10
values.push_back(40);
// `it` is valid only if push_back did not reallocate the vector.
For std::vector, appending can require a larger allocation. If that happens, all iterators, references, and pointers to elements are invalidated.
Use reserve() when you know approximately how many elements you will append:
#include <vector>
std::vector<int> values;
values.reserve(10);
values.push_back(10);
auto it = values.begin();
values.push_back(20); // No reallocation while size stays within capacity.
int first = *it;
Step by Step Execution
Consider a vector with no spare capacity:
#include <vector>
std::vector<int> numbers{1, 2, 3};
auto it = numbers.begin();
numbers.push_back(4);
// Do not assume that *it is safe here.
A possible sequence of events is:
numbersstores1,2, and3in a memory block.itstores enough information to refer to the first element in that block.push_back(4)finds that the current block is full.- The vector allocates a larger block elsewhere.
- The values are moved or copied into the new block.
- The old block is released.
itnow refers to storage that is no longer owned by the vector, so it is invalid.
A safe approach is to use a new iterator after the modifying operation:
numbers.push_back(4);
auto first = numbers.begin();
value = *first;
Real World Use Cases
Iterator invalidation matters whenever a program keeps a position in a container while modifying that container.
- Filtering records: remove inactive users from a
std::vector<User>without skipping elements or using an invalid iterator. - Event queues: append events to a vector while processing a batch. Reserve capacity or avoid retaining iterators across appends.
- Caches: use
std::unordered_mapfor lookup, while remembering that insertion can trigger a rehash and invalidate iterators. - Task schedulers: use
std::listwhen tasks must be removed while iterating and stable iterators are valuable. - API design: avoid returning a vector iterator to callers if later API calls may grow, erase from, or replace the vector.
- Data processing: use the iterator returned by
eraseto continue a loop safely after removing matching items.
Real Codebase Usage
In production code, developers reduce invalidation bugs with a few common patterns.
Use the iterator returned by erase
Many containers return the next valid iterator from erase:
#include <vector>
for (auto it = values.begin(); it != values.end(); ) {
if (*it < 0) {
it = values.erase(it);
} else {
++it;
}
}
For a vector, erasing shifts later elements, so the original it cannot be incremented afterward. The returned iterator is the correct continuation point.
Prefer the erase-remove idiom for vectors
When removing many values from a vector, compact first and erase once:
#include <algorithm>
#include <vector>
values.erase(
std::remove_if(values.begin(), values.end(),
[](int value) { return value < 0; }),
values.());
Common Mistakes
Keeping a vector iterator after growth
std::vector<int> values{1, 2, 3};
auto it = values.begin();
values.push_back(4);
std::cout << *it; // Potentially undefined behavior
push_back may reallocate. Reserve enough capacity before saving the iterator, or reacquire it afterward.
Incrementing an iterator after erasing it
for (auto it = values.begin(); it != values.end(); ++it) {
if (*it == 0) {
values.erase(it); // `it` is invalid now
}
}
Use the iterator returned by erase instead.
Assuming end() remains valid
auto last = values.end();
values.push_back(42);
if (values.end() == last) {
// Do not rely on this comparison.
}
Comparisons
| Container | Insertion | Erasure | Notes |
|---|---|---|---|
std::vector | Reallocation invalidates all iterators, references, and pointers. Without reallocation, insertion invalidates iterators at or after the insertion point. | Invalidates iterators, references, and pointers at or after the erased position. | end() is invalidated by size changes. |
std::deque | Insertion invalidates iterators. References and pointers to existing elements remain valid for insertion at either end, but do not assume iterator stability. | Erasing at an end invalidates only iterators/references to erased elements; erasing in the middle invalidates all iterators and references. | Deque iterator rules are less intuitive than vector rules. |
std::list | Existing iterators, references, and pointers remain valid. | Only iterators, references, and pointers to erased elements are invalidated. | Nodes are individually allocated and linked. |
Cheat Sheet
- An invalid iterator must not be dereferenced, incremented, compared, or passed to container functions.
- Removing an element always invalidates iterators, references, and pointers to that element.
std::vectorreallocation invalidates everything into the vector.- Vector insertion/erasure without reallocation invalidates positions at or after the changed position.
- Vector
push_backinvalidates the oldend()iterator even if no reallocation happens. std::list,std::forward_list,std::map, andstd::setkeep iterators to other elements valid when inserting or erasing.std::unordered_mapinsertion can invalidate iterators if it causes a rehash; references and pointers to existing elements survive rehashing.rehash()andreserve()on unordered containers invalidate iterators.- Prefer
it = container.erase(it)when erasing during iteration. - Reacquire
begin()orend()after a container-changing operation when unsure. - After
swap, element iterators refer to elements now owned by the other container; reacquireend().
FAQ
What does iterator invalidation mean in C++?
It means an iterator no longer safely refers to a usable position in its container. Using it afterward can cause undefined behavior.
Does std::vector::push_back invalidate iterators?
It invalidates all iterators, references, and pointers if it reallocates. Without reallocation, iterators to existing elements remain valid, but the previous end() iterator is invalidated.
Does std::vector::erase invalidate every iterator?
No. It invalidates iterators, references, and pointers at or after the erased position. Iterators before that position remain valid.
Which C++ containers have stable iterators during insertion?
std::list, std::forward_list, std::map, and std::set keep iterators to existing elements valid during insertion.
Does std::unordered_map::insert invalidate iterators?
It can. If insertion causes a rehash, all iterators are invalidated. References and pointers to existing elements remain valid unless their element is erased.
Can reserve() prevent vector iterator invalidation?
It can prevent invalidation caused by future vector reallocations while the size remains within capacity. It does not prevent invalidation caused by inserting or erasing in the middle.
Is it safe to erase elements while iterating?
Mini Project
Description
Build a small cleanup function for a list of integer measurements. The function removes invalid negative measurements while iterating over a std::vector. This demonstrates the safe erase-loop pattern and why the return value from erase matters.
Goal
Create a function that removes all negative integers from a vector without using an invalidated iterator.
Requirements
Use a std::vector<int> containing both positive and negative values.
Remove every negative value while traversing the vector.
Use the iterator returned by vector::erase after an element is removed.
Print the remaining values after cleanup.
Keep learning
Related questions
Advantages of Brace Initialization in C++
Learn why C++ brace initialization is often clearer and safer than other object initialization styles, with examples and common pitfalls.
Basic Rules and Idioms for Operator Overloading in C++
Learn the core rules, syntax, and common idioms for operator overloading in C++, including member vs non-member operators.
C++ Aggregates, Trivial Types, Trivially Copyable Types, and PODs Explained
Learn what aggregates, trivial types, trivially copyable types, and PODs mean in C++, how they differ, and why they matter.