2Brute2Best
πŸ”₯0
M5.5

Iterator Categories, Invalidation Rules & Essential STL Algorithms

Medium High-frequency interview topic Pro Only5 Iterator CategoriesComplete Invalidation TableErase-Remove / std::erase_ifsort vs nth_element
⚑ 15-Second Mental ModelM5.5 · Instant Intuition
Single-Read Mastery
The Real-World Physical Analogy (Core Intuition)

In modern C++, we never guess what the compiler does. We look directly at byte offsets, CPU registers, and object ownership lifetimes.

Rule to Remember: Make illegal states impossible to compile or run.
3 Golden Facts to Remember
Fact #1
What is the precise distinction between iterator invalidation and reference invalidation?

An iterator is an abstraction holding both a memory pointer and container traversal metadata. A reference is a direct alias to the object in RAM. In node-based containers (unordered_map on rehash, deque on push), the bucket table or chunk map may move, which invalidates iterators because their traversal path is broken. However, the underlying node memory is untouched, so references and raw pointers to existing elements remain 100% valid.

Fact #2
Under what exact conditions does std::vector::push_back invalidate iterators?

If v.size() < v.capacity(), memory is not reallocated: existing elements do not move, so all iterators to existing elements remain valid, and only v.end() is invalidated. If v.size() == v.capacity(), the vector allocates a larger memory block and moves/copies all elements, invalidating ALL iterators, pointers, and references without exception.

Fact #3
Why does std::unordered_map rehash invalidate iterators but NOT references?

std::unordered_map is a closed-addressing hash table using separate chaining. The container owns a dynamic array of bucket pointers (Node**). When rehash() runs, it allocates a larger array of bucket pointers and re-links each Node* into its new bucket. The Node structs storing key-value pairs are never reallocated or copied. Thus, iterators (which iterate across the bucket array) are invalidated, but references to pair<const Key, Value> inside the nodes remain valid.

01

Interviewer's lens

What separates a strong C++ candidate from a textbook answer

Interviewers obsess over iterator invalidation because it separates memorized trivia from genuine understanding of memory topology:

  • Textbook trap: Blindly repeating 'vector invalidates on push' without understanding why or when.
  • Hardware reality: Iterators are pointers wrapped in traversal logic. They break for 3 distinct physical reasons: heap buffer reallocation, contiguous memory shifting, or heap node deletion.
  • Senior skills expected: Distinguishing iterator invalidation vs reference/pointer invalidation (e.g. why unordered_map rehash destroys iterators while references stay 100% valid), writing crash-free mutation loops with it = c.erase(it), applying modern C++20 std::erase_if, and knowing why std::sort fails on std::list.
02

Under the hood & memory layout

Hardware & memory representation

An iterator is simply a bookmark holding a pointer into container memory. When a container changes, the bookmark breaks under 3 distinct physical mechanics:

  • 1. Moving the Entire Building (std::vector Reallocation): When a vector runs out of capacity, it moves every element to a brand new heap block and tears down the old one. Every bookmark, pointer, and reference to the old block immediately dangles.
  • 2. Sliding Seats in a Row (std::vector Erase): Erasing index 2 shifts elements 3, 4, 5 left by one slot (memmove). The memory block stays, but bookmarks pointing to the erased slot or anything behind it now point to shifted data or dead space.
  • 3. Snapping a Single Thread (std::list, std::map): Each element is an isolated node. Erasing an item deletes only that specific node, leaving every other node and bookmark completely untouched.
  • 4. Replacing the Mailbox Rack (std::unordered_map Rehash): Rehashing reallocates the bucket pointer array (breaking all bucket-traversal iterators), but leaves individual element nodes untouched in memory (preserving all element references).
Vector Buffer Reallocation vs Contiguous Shift

If size == capacity, push_back allocates a new 2Γ— buffer elsewhere in heap memory, calls move/copy constructors, and frees the old buffer, turning all old pointers and iterators into dangling addresses. If size < capacity, only end() invalidates. erase(it) performs a memmove/move loop shifting $[it+1, end)$ left, invalidating pointers into the shifted region.

Deque Chunk Map vs Page Element Stability

std::deque allocates elements in fixed-size blocks (e.g. 512 bytes) addressed via a central map of chunk pointers. Inserting at front() or back() may reallocate the central chunk map (invalidating iterators), but existing element chunks are never moved, keeping all references and pointers to elements completely valid.

Node-Based Isolation (list, set, map)

Each element is allocated independently via new Node{val, prev, next}. Erasing an element unlinks its pointers and calls delete node. Only iterators pointing to the deleted node are invalidated; all other iterators, pointers, and references remain intact.

Hash Table Rehash: Bucket Array vs Node Storage

On rehash, std::unordered_map allocates a new larger array of bucket head pointers and re-links each node into its new bucket. Because the nodes themselves are not reallocated or copied, all references and pointers to std::pair<const Key, Value> remain valid, while all iterators (which traverse bucket lists) are invalidated.

03

Core concepts & syntax traps

01

The 5 Iterator Categories & Why std::sort Fails on std::list

C++ iterator categories define what physical navigation a container's memory topology supports:

  1. Input Iterator: Single-pass read-only stream, advance only via ++ (e.g. std::istream_iterator).
  2. Forward Iterator: Multi-pass read/write, advance only via ++ (e.g. std::forward_list).
  3. Bidirectional Iterator: Step forward with ++ and backward with --, but cannot jump (e.g. std::list, std::set, std::map).
  4. Random-Access Iterator: Jump arbitrary offsets in O(1) via it + n, subtract iterators it2 - it1, and subscript it[n] (e.g. std::deque).
  5. Contiguous Iterator (C++20): Random access with elements guaranteed to lie in adjacent physical memory bytes: &*(it + n) == (&*it) + n (e.g. std::vector, std::array, std::string, std::span).

Why std::sort Rejects std::list: std::sort uses Introsort (QuickSort hybrid), which requires jumping to middle pivots (first + (last - first)/2) in O(1) time. Because std::list::iterator is only Bidirectional, it lacks operator+ and operator-. Calling std::sort(lst.begin(), lst.end()) fails compilation! Instead, call the member function lst.sort(), which executes an O(N log N) merge sort by relinking node pointers with zero element copies.

C++23 snippet
std::vector<int> v = {4, 2, 5, 1};
std::sort(v.begin(), v.end()); // OK: contiguous random-access

std::list<int> l = {4, 2, 5, 1};
// std::sort(l.begin(), l.end()); // COMPILER ERROR: list lacks random-access!
l.sort(); // OK: member function relinks node pointers in O(N log N)
Interview takeaway:

Algorithms requiring random access (std::sort, std::nth_element, std::binary_search) cannot run on std::list; use member functions or advance iterators manually.

02

The Master STL Invalidation Table: Iterators vs References

Senior interviewers test whether you realize that an iterator can die while a reference lives on:

  • An iterator tracks container traversal logic (e.g. current bucket + position in chain).
  • A reference or raw pointer points directly to the element's raw memory address.
ContainerOperationIterators InvalidatedReferences & Pointers Invalidated
std::vectorpush_back (full capacity)ALL (buffer moved)ALL (old buffer freed)
std::vectorpush_back (room available)Only end()NONE (buffer untouched)
std::vectorerase(it)it and everything afterit and everything after
std::dequepush_front / push_backALL (map may resize)NONE (100% stable in chunks!)
std::dequeerase (in middle)ALLALL
std::list / map / setinsertNONENONE
std::list / map / seterase(it)ONLY itONLY it
std::unordered_mapinsert (triggers rehash)ALL (buckets reallocated)NONE (nodes never move!)
std::unordered_maperase(it)ONLY itONLY it
C++23 snippet
std::unordered_map<int, std::string> map = {{1, "alpha"}};
const std::string& ref = map[1]; // bind reference

// Force rehashing:
map.rehash(1000);

// Iterators are invalid, but the reference remains completely valid!
std::println("{}", ref); // Prints "alpha" safely: node address did not change
Interview takeaway:

In std::deque (push ends) and std::unordered_map (rehash), all iterators are invalidated while existing element references remain 100% valid.

03

The Classic Loop Crash & Safe Deletion via it = c.erase(it)

Erasing an element inside a naive for loop is the #1 STL bug in production:

// DEADLY BUG: invalidates 'it' before increment!
for (auto it = c.begin(); it != c.end(); ++it) {
    if (shouldDelete(*it)) c.erase(it);
}

Why This Breaks:

  1. Node Containers (list, map) -> Use-After-Free: Calling c.erase(it) deallocates the node immediately. The loop header then executes ++it, attempting to read it->next from freed heap memory. Result: segfault or silent memory corruption.
  2. Contiguous Containers (vector, deque) -> Skipped Elements: Erasing shifts all subsequent elements left by one slot. The next element slides directly into it's current slot. When the loop header does ++it, it skips that element completely!

The Universal Idiomatic Fix: Control iterator advancing manually: assign it = c.erase(it) when deleting, and only execute ++it when keeping the element:

for (auto it = c.begin(); it != c.end(); /* manual advance */) {
    if (shouldDelete(*it)) {
        it = c.erase(it); // Returns iterator to next valid element!
    } else {
        ++it;
    }
}
C++23 snippet
std::vector<int> v = {10, 20, 25, 30};
for (auto it = v.begin(); it != v.end(); ) {
    if (*it % 2 != 0) {
        it = v.erase(it); // returns iterator pointing to 30
    } else {
        ++it;
    }
}
Interview takeaway:

Always control loop advancing manually when erasing: it = c.erase(it) on deletion, ++it otherwise.

04

The Erase-Remove Idiom vs C++20 std::erase_if

Deleting elements one by one with v.erase(it) inside a vector loop runs in O(N^2) time because every erase shifts all remaining elements left.

The Classic Erase-Remove Idiom (O(N)):

v.erase(std::remove(v.begin(), v.end(), val), v.end());
  • Why two steps? std::remove operates only on iterators. It has no access to the container itself, so it cannot change v.size(). It slides kept elements to the front and returns an iterator pointing to the first garbage tail element.
  • The outer v.erase(new_end, v.end()) call actually truncates the container and updates size().

The Modern C++20 Solution: C++20 obsoletes this boilerplate with unified non-member functions:

std::erase(v, val);                                    // Erase matching values
std::erase_if(v, [](int x) { return x % 2 == 0; });   // Erase by predicate

Runs in linear O(N) time across vector, deque, list, map, and unordered_map with zero boilerplate.

C++23 snippet
std::vector<int> nums = {1, 2, 3, 4, 5, 6};

// Pre-C++20 (Erase-Remove Idiom):
nums.erase(std::remove_if(nums.begin(), nums.end(), [](int x) { return x % 2 == 0; }), nums.end());

// Modern C++20:
std::erase_if(nums, [](int x) { return x % 2 == 0; });
Interview takeaway:

Use C++20 std::erase_if for clean, bug-free O(N) element filtering across all STL containers.

05

std::sort vs std::stable_sort vs O(N) std::nth_element

Selecting the right algorithm is a classic systems design question:

  1. std::sort (Unstable O(N log N)):
    • Uses Introsort (starts with QuickSort, switches to HeapSort if recursion depth exceeds 2 log N to prevent O(N^2) worst cases, and finishes with InsertionSort on small partitions).
    • Not stable: Equivalent elements may swap relative order.
  2. std::stable_sort (Stable O(N log N)):
    • Uses MergeSort to preserve the relative ordering of equivalent keys.
    • Allocates a temporary buffer of up to N/2 elements. If memory allocation fails, it degrades to O(N log^2 N) in-place sorting.
  3. std::nth_element (Linear O(N) Selection):
    • Uses Introselect to partition elements such that the element at index nth is the exact element that would be there if the range were fully sorted.
    • Elements before nth are <= *nth; elements after are >= *nth.
    • Performance Superpower: When computing medians, 99th percentiles, or top-K cutoffs, std::nth_element avoids sorting sub-partitions, running up to 10x faster than std::sort.
C++23 snippet
std::vector<int> latencies = {120, 45, 900, 310, 50, 720, 15, 600};

// Find median in strictly linear O(N) time:
auto medianIt = latencies.begin() + latencies.size() / 2;
std::nth_element(latencies.begin(), medianIt, latencies.end());
std::println("Median latency: {}", *medianIt);
Interview takeaway:

Use std::nth_element for O(N) percentile and median computation instead of fully sorting with std::sort.

Brute2Best Pro Content

Unlock Live Implementation & Interview Scenarios

The C++23 production code, live coding steps, scenario twist solutions, and common UB traps are part of Brute2Best Pro. Module 1 is completely free.

All 440 visual dry runs & algorithms
Interactive step-by-step playback with pointer & array state transitions
52 comprehensive LLD chapters
Interactive UML diagrams, real-world stories (Food Delivery, Trading, RPG), and practice solutions
40 C++ basics labs & hardware memory models
Vtables, byte layouts, assembly insights, and multi-threading race conditions
Production-ready C++23 source code
Modern idioms, clean contracts, time/space complexity derivations, and interview takeaways

First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.

Next In Your Mastery Journey

Apply C++ Internals to LLD & Algorithmic Patterns

Full-stack systems engineering