C++ / Editorial solution guide

Fix iterator invalidation after container changes

Identify the exact modifying operation and container rule, then choose a traversal that remains valid as elements move or are erased.

About 3 min readContent reviewed

Read-only knowledge base ยท No public comments

Recognize the symptoms

A loop fails after push_back or erase, a saved element reference changes unexpectedly, or a debug iterator mode reports an invalid iterator. An ordinary build may appear to work because old storage has not visibly changed. That appearance is not a validity guarantee and must not be promoted to an expected output.

Problems often depend on input size because vector growth may reallocate only after capacity is exhausted. A larger reserve can hide the symptom while leaving erasure rules, cached end iterators, or future growth assumptions wrong. Diagnose the operation, not just the size that triggered failure.

Understand the likely causes

  • A vector reallocation invalidates all references, pointers, and iterators into its old element storage. Without reallocation, push_back still invalidates the old past-the-end iterator. erase invalidates access positions at or after the erased element, including the old end. Different containers have different rules, so a rule learned for list does not transfer to vector.
  • Range-for obtains traversal state before iteration. Appending to the same vector can invalidate that state even if the current loop variable is a copied int. Saving an index avoids a stale address only when the logical position remains meaningful; erasing or reordering earlier items can change its identity.

Mark the invalidating operation

List every saved borrower before the mutation: element references, iterators, data pointers, spans, and cached end values. The excerpt below intentionally keeps a reference across possible reallocation. It is non-runnable and no output is promised. Check whether the operation changes size, capacity, ordering, or all three. reserve does not make arbitrary future mutations safe, and capacity is not the number of constructed elements.

cpp Unsafe assumption about a saved reference
#include <vector>
void broken() {
    std::vector<int> values{1};
    int& saved = values[0];
    values.push_back(2);
    saved = 9; // invalid if growth reallocated storage
}

No execution is needed. push_back may invalidate saved, so the final write has no generally valid lifetime argument.

Continue from the position returned by erase

When erasing during traversal, use the valid next iterator returned by erase instead of incrementing the erased iterator. Increment only when no erasure happened. The complete program removes even values while keeping every access in the current valid range. For simple filtering, erase-remove or C++20 std::erase_if may express the same intention more directly, but the loop makes the iterator transition explicit.

cpp A valid erase traversal
#include <iostream>
#include <vector>
int main() {
    std::vector<int> values{1, 2, 3, 4, 5};
    for (auto position = values.begin(); position != values.end();) {
        if (*position % 2 == 0) { position = values.erase(position); }
        else { ++position; }
    }
    for (int value : values) { std::cout << value << " "; }
    std::cout << "size=" << values.size() << "\n";
}
Expected output
1 3 5 size=3

Two and four are erased. The returned iterator identifies the next current element, and the loop compares against the current end.

Separate traversal from structural growth

If processing must append new records, collect them in a separate vector and append after the reading traversal completes. Alternatively, use an index loop over a fixed original count, reacquiring each element after any growth and never retaining element references across it. For stable identity across deletion or reordering, store application IDs and look them up. Choose another container only after checking its own stability, locality, and complexity tradeoffs.

Verify the fix

Run the corrected complete program in C++20 mode and compare its exact line: 1 3 5 size=3. Add empty, all-erased, none-erased, and adjacent-erased cases. These exercise termination and the returned-position path rather than relying on one capacity scenario.

Use libstdc++ debug mode in a consistent dedicated build to check iterator/container relationships. Keep ABI-sensitive debug settings consistent across linked components. Memory sanitizers provide complementary evidence, but a stale iterator can remain inside allocated storage and escape ordinary memory checks. Do not execute the broken excerpt.

Keep it from recurring

  • Record invalidation rules in interfaces that return element access. Keep structural edits outside reading traversals where practical, reacquire borrowers after mutations, and test boundary shapes rather than hoping reserve eliminates the problem.

Official references