An iterator can look like a saved position, but it is valid only while the container's rules permit its use. A vector can move its storage when it grows. Erasing a value shifts later elements. Even a saved end iterator can become invalid after mutation. Using an invalidated handle is not a reliable way to find the “same” element again.
This guide uses C++17. You will erase values through the returned iterator and retain a stable record identifier across vector changes. The examples deliberately avoid dereferencing invalidated handles. A container's operation-specific contract, rather than whether one test happened to crash, determines what remains valid.
A handle's validity is a separate property
Iterators support operations under their category and associated range. Pointers and references can also refer to container elements. All three depend on storage and element lifetime, but an operation can affect them differently. Review the exact rules for the container and operation instead of memorizing “all mutations invalidate everything.”
A past-the-end iterator identifies a boundary and must not be dereferenced even while it is valid. Mutation can invalidate that boundary without destroying every earlier element. Range algorithms also need a coherent pair of iterators from the intended range. Mixing a new begin with a stale end does not create a valid range. Keep handle acquisition close to the operation that uses it so the lifetime argument remains easy to review.
Vector growth and insertion have precise consequences
When vector growth causes reallocation, iterators, pointers and references to its elements are invalidated. Reserving sufficient capacity before acquiring handles can avoid reallocation for a known amount of growth, but it does not make every later vector mutation harmless. An insertion without reallocation invalidates applicable handles at or after the insertion point, including the old end.
Reserve itself can reallocate, so perform it before keeping element handles. Capacity is not a business input limit, and assuming an implementation's growth factor is not a portable proof. An index avoids a raw pointer to moved storage, but it can still identify a different element after insertions or erasures before that position. For persistent logical identity, use a stable key rather than a position.
Erase returns the next valid position
For vector erase, handles at or after the erasure point are invalidated. A loop should assign the iterator returned by erase instead of incrementing the erased iterator. When no erasure occurs, advance normally. This directly matches the changing range and prevents skipping an element.
The erase-remove idiom is another useful design when a single pass can partition the values and the tail is then erased. The remove algorithm alone does not change the container's size. Choose the style based on what the operation needs, not just brevity. Do not keep references to elements across the compaction step unless the relevant contract explicitly supports the intended use.
Other containers have different tradeoffs
A list generally preserves handles to elements that are not erased when other elements are inserted or removed. That stability comes with a different storage and traversal cost profile. A deque has its own detailed rules for end and middle operations; it is not simply a vector with universally stable iterators.
An unordered container's rehash invalidates iterators while preserving pointers and references to existing elements under its specified rules. Erasing the referred element still ends its lifetime. A map's node-based behavior differs from vector's contiguous shifting. These differences can help select a container, but do not choose a more expensive structure merely to preserve handles the design does not actually need. Explicit logical identifiers can often remove the need to retain fragile traversal state.
Invalidation is not detected by every build
A stale iterator can appear to work when old memory remains mapped, making the bug intermittent. A debug iterator mode or sanitizer can reveal some errors, but their silence is not a proof of validity. Compiler optimization and a different allocation pattern can change the observed result.
Write tests for zero removals, all removals, consecutive removals and insertion near capacity. These cases exercise the safe algorithm without intentionally invoking undefined behavior. During review, mark every retained iterator, reference, pointer and view, then identify intervening operations that could invalidate it. A short mutation phase followed by fresh lookup is often easier to maintain than a complex promise that no future edit will trigger reallocation.
Ownership and concurrency remain separate concerns
Valid handles in a single-threaded mutation sequence do not make unsynchronized concurrent access safe. A container operation can race with another reader before invalidation is even considered. Define synchronization or exclusive ownership first, then apply the container's validity rules inside that boundary.
Likewise, replacing an owner object or destroying it invalidates borrowed access even if no container modifier was called. Spans and string views follow the same owner-lifetime principle. Keep APIs honest about whether a returned handle is temporary traversal state or a durable domain identifier. The latter should normally be a stable value that can be resolved again under the current state.
Official references
C++ Iterator and Reference Invalidation Examples
Each example shows the code, the output it produces and what each part does.
Erase even values safely
The complete program filters a vector in place. Every erasing step uses the new iterator supplied by the container.
The end boundary is reacquired by each loop condition. Advancing the old iterator after erase would not preserve this valid traversal. The code never relies on the addresses of shifted elements.
#include <iostream>
#include <vector>
int main() {
std::vector<int> values{1, 2, 3, 4, 5};
for (auto it = values.begin(); it != values.end();) {
if (*it % 2 == 0)
it = values.erase(it);
else
++it;
}
for (int value : values)
std::cout << value << '\n';
}1
3
5C++ Iterator and Reference Invalidation in Real Projects
Worked exercise: reacquire by record identifier
The task is to keep track of order twenty while removing completed orders and adding a new order. Save its logical ID, mutate the vector, and search again afterward. Do not retain the earlier iterator or reference. The ID contract requires uniqueness, which this small fixture satisfies.
#include <algorithm>
#include <iostream>
#include <vector>
struct Order {
int id;
bool complete;
};
int main() {
std::vector<Order> orders{{10, true}, {20, false}, {30, true}};
const int selected_id = 20;
orders.erase(
std::remove_if(orders.begin(), orders.end(),
[](const Order& order) { return order.complete; }),
orders.end());
orders.push_back({40, false});
auto selected = std::find_if(orders.begin(), orders.end(),
[id = selected_id](const Order& order) {
return order.id == id;
});
if (selected == orders.end())
std::cout << "Order no longer present\n";
else
std::cout << "Order " << selected->id << ": pending\n";
std::cout << "Remaining orders: " << orders.size() << '\n';
}Order 20: pending
Remaining orders: 2Whether push_back reallocates is irrelevant because the search happens after all mutations. If another operation can remove the selected order, the missing branch handles that outcome. For frequent lookup, a key-based container can be appropriate, but its own mutation rules still need review.
C++ Iterator and Reference Invalidation FAQ
Does reserve make vector iterators permanently stable?
No. It can prevent reallocation for bounded growth when called before handles are acquired, but insertion and erasure still have invalidation rules. Growth beyond the reserved capacity can reallocate, and reserve itself may move storage. Treat capacity planning as one part of a specific validity argument, not a permanent stability guarantee for every operation.
Why not increment the iterator after erase?
The erased iterator is no longer a valid traversal position. The erase operation returns a valid iterator to the following element or the new end, so use that result. Incrementing an invalid iterator is not repaired by checking it afterward. Tests with adjacent removable elements are useful because an incorrect loop can skip values even before an obvious crash occurs.
Is an index always safer than an iterator?
An index does not point into an old allocation, but insertions and erasures can change which element occupies that position. It can also become out of range. For logical identity across mutation, save a stable key and resolve it again. An index is useful when the requirement truly describes a position and the code validates the current range before access.
Can sanitizers prove my iterator code is correct?
They can detect many memory errors in executed paths, but they do not establish every container precondition or future interleaving. Some invalidation errors access still-mapped memory and may escape a sanitizer. Use tools as additional evidence alongside the operation-specific rules and safe algorithm structure. Never accept undefined behavior merely because one debug run produced the expected text.
C++ Iterator and Reference Invalidation Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
An order must be identified after a vector is filtered and extended. Which design is robust?
C++ Iterator and Reference Invalidation Quiz
2 questions to check what you learned.
What should an erase-while-iterating loop do after erasing the current element?
- Increment the erased iterator.
- Assign the iterator returned by erase.
- Dereference the old end iterator.
- Assume the original range is unchanged.
A vector reallocates while growing. What happens to old element pointers?
- Only pointers to the final element are affected.
- They automatically track the new allocation.
- They are invalidated and must not be used as though still valid.
- Calling reserve after reallocation repairs every previously saved pointer.