Skip to content

Chapter 11 — STL Containers, Algorithms, and Iterator Invalidation

Chapter 11 — STL Containers, Algorithms, and Iterator Invalidation

The container map

C# C++ Notes
List<T> std::vector<T> your default, 95% of the time
Dictionary<K,V> std::unordered_map<K,V> hash table, O(1)
SortedDictionary<K,V> std::map<K,V> tree, O(log n), sorted iteration
HashSet<T> std::unordered_set<T>
Queue<T> / Stack<T> std::queue / std::stack
LinkedList<T> std::list<T> almost never the right choice
T[] std::vector<T>, or std::array<T, N> when N is fixed a C# T[] sizes itself at runtime, so vector is the general match; array needs N at compile time and stores its elements inline — no heap block of its own

Gotcha: plain std::map is the TREE (sorted, O(log n)); the Dictionary equivalent is unordered_map. "I'd use unordered_map for lookups unless I need sorted order."

That table maps the names. Choosing between them — including the column it does not have, whether an element keeps its address when the container grows — is Appendix H, which turns this chapter's mechanisms into a procedure.

Why vector dominates — cache locality

A vector is one contiguous memory block. Even where a list is theoretically better, vector usually wins because CPUs prefetch contiguous memory. push_back is amortized O(1): when capacity runs out, the vector reallocates (typically doubling) and moves everything to a new block — remember that reallocation, it matters below.

Key operations

std::vector<Widget> v;
v.push_back(w);
v.emplace_back("name", 5);   // construct in place - prefer it
v[3];                        // no bounds check (fast, UB if out of range)
v.at(3);                     // bounds-checked, throws - like C# indexer

std::unordered_map<std::string, Widget> m;
m["wall"] = w;               // insert or overwrite
// TRAP: reading with [] INSERTS a default value if key missing!
auto it = m.find("wall");    // the safe lookup
if (it != m.end()) Use(it->second);   // ->first key, ->second value
if (m.contains("wall")) ...  // C++20, like ContainsKey

Try it (30 seconds). Predict m.size() after if (m["answer"] == 42) {} on an empty map — then print it. The comparison read nothing; it made something.

Iterators

An iterator is a generalized pointer: begin() points at the first element, end() points one past the last (a sentinel — never dereference it). The half-open range [begin, end) is the universal STL currency.

auto it = std::find(v.begin(), v.end(), target);
if (it != v.end()) {   // "not found" == end, the idiom
    Use(*it);
}

Two spellings of one search, and they are not interchangeable: std::find_if with a key-comparing lambda over a std::map compiles and visits every node, while the member m.find(key) is the O(log n) — or O(1) — lookup you meant. Recipe 18 in Appendix F puts the three spellings of "not found" side by side.

Algorithms — LINQ's rough equivalent

LINQ STL (<algorithm> / <numeric>)
FirstOrDefault(pred) std::find_if(begin, end, pred) — returns iterator
Count(pred) std::count_if(begin, end, pred)
Any / All std::any_of / all_of / none_of
OrderBy std::sort(begin, end, cmp) — but IN PLACE, and unstable: std::stable_sort is the real match
Select std::transform(begin, end, std::back_inserter(out), func)
Aggregate / Sum std::accumulate(begin, end, 0)
Max std::max_element — returns ITERATOR to max

Mindset shifts: STL algorithms mutate in place (LINQ returns new lazy sequences), and "Where" is awkward pre-C++20 — the classic filter is the famous erase-remove idiom:

// remove all small widgets - the pre-C++20 incantation:
v.erase(std::remove_if(v.begin(), v.end(),
        [](const Widget& w){ return w.size < 10; }),
        v.end());
// remove_if only SHIFTS survivors forward and returns the new logical
// end; erase then chops the garbage tail. Two steps, always paired.

std::erase_if(v, [](const Widget& w){ return w.size < 10; });  // C++20

THE trap: iterator invalidation

The C# equivalent — modifying a collection during foreach — throws immediately. C++ gives you undefined behavior: maybe a crash, maybe silent corruption, maybe it works on your machine and dies in production.

// BUG - the classic:
for (auto it = v.begin(); it != v.end(); ++it) {
    if (it->size < 10)
        v.erase(it);       // 'it' is now INVALID; ++it next loop is UB
}

// FIX - erase returns the next valid iterator:
for (auto it = v.begin(); it != v.end(); /* nothing */) {
    if (it->size < 10)
        it = v.erase(it);  // step forward via the return value
    else
        ++it;
}
// or better: std::erase_if(v, pred);

Worse — push_back can invalidate everything too: if the vector grows, the whole block moves, and every iterator, pointer, and reference into it dangles. Appending while iterating is UB even though you deleted nothing.

Invalidation rules to memorize: vector — insert/erase invalidates iterators at/after the point, and ALL of them if reallocation happens. unordered_map — insertion can invalidate iterators (rehash) but references survive; erase kills only the erased. map/list — iterators stable except the erased element.

In the wild: C-style SDKs

Vendor container libraries (Qt, Unreal, and countless in-house ones) mirror the STL: same concepts, same invalidation logic, different spelling. C APIs additionally return dynamic arrays via pointer+count pairs or opaque handles (Chapter 17's payload pattern), which you often immediately wrap or copy into proper containers so the rest of your code lives in STL-land.