Containers
STL container choice, complexity, iterators, and invalidation.
31 questions
JuniorTheoryVery commonHow do you choose between vector, list, map, set, and unordered_map?
How do you choose between vector, list, map, set, and unordered_map?
Default to vector for cache-friendly iteration. Use map/set for sorted O(log n) lookup, unordered_map/set for average O(1) without ordering, and list only when stable iterators or O(1) splice outweigh cache locality.
Common mistakes
- ✗Choosing list because insertion is O(1) while ignoring search and cache costs
- ✗Expecting unordered_map iteration to be sorted
- ✗Ignoring worst-case hashing behavior
Follow-up questions
- →Which container would you use for an LRU cache?
- →Why is vector often faster than list?
JuniorTheoryVery commonWhen would you choose std::array over std::vector and vice versa?
When would you choose std::array over std::vector and vice versa?
Use std::array<T, N> for compile-time fixed sizes — stack allocation, zero-overhead C-array wrapper. Use std::vector<T> for runtime-dynamic sizes or large buffers; it owns a heap block with capacity-based growth.
Common mistakes
- ✗Putting a huge
std::array<T, 1'000'000>on the stack — overflow - ✗Using
std::vectorfor a fixed-size 3-element coordinate — wastes a heap allocation - ✗Forgetting that
std::array<int, 0>is valid andempty()returns true
Follow-up questions
- →How does CTAD work for
std::array(deduction from braced list)? - →Can
std::arraybeconstexpr?
MiddleDebuggingVery commonWhy is erasing in this loop undefined behavior?
Why is erasing in this loop undefined behavior?
vector::erase invalidates it and every iterator after it; the next ++it then advances a dangling iterator — UB. Fix: use the return value, it = v.erase(it);, and only ++it when you did not erase. Or std::erase_if(v, pred); (C++20).
Common mistakes
- ✗Believing erase leaves the iterator pointing at the next element
- ✗Blaming repeated end() calls instead of iterator invalidation
- ✗Thinking the erase-remove idiom is unnecessary here
Follow-up questions
- →How does the erase-remove idiom avoid this problem entirely?
- →Which containers do NOT invalidate other iterators on erase?
MiddleTheoryVery commonWhat is iterator invalidation and how does it differ across containers?
What is iterator invalidation and how does it differ across containers?
Invalidation means an iterator no longer points to a valid element after a mutation. Vector reallocation invalidates everything; list/map insert keeps iterators valid; unordered_map rehash invalidates iterators.
Common mistakes
- ✗Keeping vector iterators across push_back without reserve
- ✗Erasing from a container in a loop without using the returned iterator
- ✗Assuming all references are invalidated whenever iterators are
Follow-up questions
- →Show the correct erase-while-iterating pattern.
- →How does reserve affect vector invalidation?
MiddleDebuggingVery commonWhy does *p dangle after push_back?
Why does *p dangle after push_back?
push_back can reallocate the vector's storage when it grows past capacity, invalidating p, which now dangles — UB. Pointers, references, and iterators into a vector are invalidated by reallocation. Fix: re-acquire p after the insert, or reserve capacity up front.
Common mistakes
- ✗Believing push_back never moves existing elements
- ✗Thinking raw pointers survive vector reallocation while iterators do not
- ✗Forgetting that capacity growth relocates the whole buffer
Follow-up questions
- →When exactly does
vector::push_backinvalidate references and iterators? - →How does
reservemake a sequence ofpush_backs pointer-stable?
JuniorTheoryCommonWhat's the difference between reserve and resize for std::vector?
What's the difference between reserve and resize for std::vector?
reserve(n) grows capacity() to at least n without constructing elements — useful before a known series of push_backs. resize(n) changes size() to n, value-initialising new elements and destroying excess ones.
Common mistakes
- ✗
v.reserve(n)thenv[i] = xinstead ofpush_back— undefined access past size - ✗Calling
resizewhen you only want capacity — pays for value-initialisation of all elements - ✗Calling
reserve(0)thinking it shrinks — useshrink_to_fit(and even that is non-binding)
Follow-up questions
- →What's the typical growth factor for
std::vectorand why? - →What does
capacity()return immediately afterclear()?
JuniorTheoryCommonHow are std::stack and std::queue implemented and what is their underlying container?
How are std::stack and std::queue implemented and what is their underlying container?
Both are adapters that wrap an underlying sequence container (default std::deque) and expose a restricted interface. std::stack gives LIFO push/pop/top; std::queue gives FIFO push/pop/front/back.
Common mistakes
- ✗Iterating over
std::stack— adapters expose no iterators by design - ✗Using
std::queue<T, std::vector>— vector lackspop_front, won't compile - ✗Forgetting that
pop()returns void; you must calltop()/front()first thenpop()
Follow-up questions
- →Why does
pop()not return the value? (exception safety) - →When would you choose
std::stack<T, std::vector<T>>over the default?
MiddleTheoryCommonHow is std::deque implemented and what are its iterator-invalidation rules?
How is std::deque implemented and what are its iterator-invalidation rules?
std::deque is a page table of pointers to fixed-size chunks: O(1) push/pop on both ends, random access, non-contiguous storage. End push/pop invalidates iterators but keeps references valid; middle insert invalidates everything.
Common mistakes
- ✗Passing
&deque[0]to a C API expecting a contiguous buffer — broken - ✗Assuming push_back doesn't invalidate iterators (it does, unlike
std::list) - ✗Choosing
dequeovervectorfor queue-like access without measuring — vector + index can be faster on cache-friendly workloads
Follow-up questions
- →Why is
std::queuea wrapper aroundstd::dequeby default? - →Compare deque to a ring buffer for FIFO workloads.
MiddlePerformanceCommonWhen does emplace_back save work compared to push_back and when is it equivalent?
When does emplace_back save work compared to push_back and when is it equivalent?
emplace_back(args...) builds the element in place from constructor args, skipping a temporary that push_back(T(a,b)) would create. With an existing T, push_back(std::move(x)) is equivalent and clearer.
Common mistakes
- ✗Using
emplace_back(existingValue)whenpush_back(existingValue)is identical and clearer - ✗Calling
emplace_backwith implicit conversions you wouldn't allow withpush_backdue toexplicit - ✗Expecting
emplace_backto skip a reallocation — it can still reallocate
Follow-up questions
- →How does
try_emplacediffer fromemplacefor maps? - →Why does
emplace_backreturn a reference (since C++17) rather than void?
MiddleTheoryCommonHow to check if a container is empty? Why is size() == 0 bad practice?
How to check if a container is empty? Why is size() == 0 bad practice?
Use container.empty() — it is O(1) for all standard containers (including pre-C++11 std::list where size() was O(n)) and expresses the intent directly.
Common mistakes
- ✗Using
size() == 0— works correctly but is a code smell;empty()is preferred - ✗Not providing
empty()in a custom container — it should bereturn size() == 0;at minimum - ✗Calling
empty()on a string and relying on it to also check for whitespace —empty()only checks zero length, not content
Follow-up questions
- →What does
std::empty(container)(free function, C++17) add overcontainer.empty()? - →Can
empty()returntruefor a container that has reserved capacity?
MiddleCodeCommonHow do you write a custom hash function for a user-defined key in unordered_map?
How do you write a custom hash function for a user-defined key in unordered_map?
Specialise std::hash<MyKey> in namespace std, or pass a hash callable as the second template argument. Combine fields with hash_combine (not trivial XOR) and provide a consistent operator==.
Common mistakes
- ✗Hashing only one field of a struct — high collision rate
- ✗Forgetting to keep
operator==consistent with the hash (equal keys must hash equal) - ✗Specialising
std::hashinside a different namespace — silently won't be found
Follow-up questions
- →What is
std::hash<std::string>typically based on (siphash, fnv, etc.)? - →How do you build a transparent hash to support heterogeneous lookup with
string_view?
MiddleTheoryCommonHow is std::list implemented internally?
How is std::list implemented internally?
std::list<T> is a doubly-linked list of individually heap-allocated nodes (value + prev + next pointers) with a sentinel head/tail. O(1) insert/erase anywhere, poor cache locality, O(1) size() since C++11.
Common mistakes
- ✗Using
std::listfor sequential access wherestd::vectorwould be significantly faster due to cache lines - ✗Iterating through a list with index arithmetic —
std::listhas nooperator[]; use iterators - ✗Splicing from one list to another and then checking original list size — splice transfers nodes, but size must be updated (O(n) in C++11 for full-list splice)
Follow-up questions
- →What is
std::forward_listand what does it trade for lower memory overhead? - →How does
std::list::sortwork given it cannot usestd::sort?
MiddleTheoryCommonWhen to use map vs unordered_map? Complexity comparison.
When to use map vs unordered_map? Complexity comparison.
std::map is a red-black tree: O(log n) ops, sorted iteration, needs only operator<. std::unordered_map is a hash table: O(1) average (O(n) worst), arbitrary order, needs operator== plus a hash.
Common mistakes
- ✗Using
unordered_mapwith a custom key without providing a hash specialisation — compile error or uses the default which may not exist - ✗Relying on
unordered_mapordering — it is unspecified and can change after rehash - ✗Ignoring worst-case O(n) for
unordered_map— can be triggered by hash-DoS attacks if keys come from untrusted input
Follow-up questions
- →How do you write a custom hash for a struct with multiple fields?
- →What is the load factor in
unordered_mapand how doesmax_load_factoraffect performance?
MiddleTheoryCommonHow does std::priority_queue order elements and how do you make a min-heap?
How does std::priority_queue order elements and how do you make a min-heap?
Default is a max-heap using std::less<T> over vector<T>; for a min-heap pass std::greater<T> as the comparator. Push/pop are O(log n) via push_heap/pop_heap; top is O(1).
Common mistakes
- ✗Trying to make a min-heap by negating values — fails for unsigned and overflows
- ✗Iterating over a
priority_queue— no iterators; you must pop to read elements - ✗Custom comparator with mutable state — comparator must be a strict weak order, not stateful
Follow-up questions
- →How do
std::make_heap,push_heap,pop_heapwork directly on a vector? - →How would you implement decrease-key (priority_queue lacks it)?
MiddleTheoryCommonWhat is the erase-remove idiom?
What is the erase-remove idiom?
std::remove only shifts non-matching elements forward and returns the new logical end; the tail is unspecified. Call container.erase(new_end, end()) to actually drop the tail — remove cannot do it because it works on any range.
Common mistakes
- ✗Calling
std::removewithout callingerase— the container retains its original size with garbage values at the end - ✗Using erase-remove on
std::list—listhas a memberremove/remove_ifthat is O(n) without moving elements, prefer it - ✗Calling erase-remove inside a range-for loop — invalidates iterators, UB
Follow-up questions
- →How does
std::erase_if(C++20) work for associative containers likestd::map? - →Why does
std::removeleave unspecified values in the tail instead of zeroing them?
MiddleTheoryCommonWhat is std::string_view and what are its lifetime pitfalls?
What is std::string_view and what are its lifetime pitfalls?
std::string_view (C++17) is a non-owning pointer+length view over a character buffer. It does not extend lifetime: a view bound to a temporary std::string is dangling immediately, so never store it past the source's life.
Common mistakes
- ✗Returning a
string_viewfrom a function that constructs astd::stringlocally — dangling - ✗Calling C functions expecting null-terminated strings on a
string_view—string_viewis not necessarily null-terminated - ✗Constructing a
string_viewfrom astd::stringrvalue — view dies with the temporary
Follow-up questions
- →When would you prefer
const std::string&overstd::string_viewfor a parameter? - →How does
string_view::data()differ fromstring_view::c_str()(no c_str exists)?
MiddleTheoryCommonHow does std::unordered_map handle hash collisions and what is bucket interface used for?
How does std::unordered_map handle hash collisions and what is bucket interface used for?
unordered_map uses separate chaining — each bucket is a linked list of colliding nodes. The bucket API (load_factor, max_load_factor, rehash) tunes when rehash fires; rehash invalidates all iterators.
Common mistakes
- ✗Assuming
unordered_mapuses open addressing — the standard mandates separate chaining (nounordered_*insert can invalidate references unless rehash) - ✗Using a poor hash function (e.g.
hash<int>is identity) and getting all keys in one bucket - ✗Not calling
reserve(n)before bulk insert — many incremental rehashes
Follow-up questions
- →Why might
std::unordered_mapbe slower thanstd::mapfor small sizes? - →How do you write a custom
HashandKeyEqual?
MiddlePerformanceCommonHow do you optimise removing an element from the middle of a vector?
How do you optimise removing an element from the middle of a vector?
Standard erase(it) is O(n) because it shifts subsequent elements left. If order is unimportant, use swap-and-pop: swap with the last element and call pop_back() — O(1).
Common mistakes
- ✗Using the swap-and-pop trick when order matters — it changes the relative position of remaining elements
- ✗Calling
erasein a loop with a forward-moving index — the index must be adjusted after each erase or iterators used carefully - ✗Not considering
std::stable_partitionwhen you want to remove many elements at once while preserving order
Follow-up questions
- →What is the iterator invalidation guarantee for
std::vector::erase? - →Benchmark: at what element size and count does
std::listbecome faster thanstd::vectorfor repeated middle erasures?
MiddleTheoryCommonHow is std::vector implemented internally?
How is std::vector implemented internally?
Three pointers (begin, end, end_of_storage) over a contiguous heap block. On overflow a 2x block is allocated, elements move (if noexcept) or copy, then the old block is freed — amortising push_back to O(1).
Common mistakes
- ✗Assuming
size() == capacity()— size is element count, capacity is allocated storage - ✗Not calling
reservebefore inserting a known number of elements — causes multiple reallocations - ✗Storing iterators or pointers into a vector and then pushing back — reallocation invalidates all iterators
Follow-up questions
- →Why does
std::vector<bool>have a special implementation and what are its pitfalls? - →What is the difference between
shrink_to_fitandclear?
JuniorTheoryOccasionalName the iterator categories and which container provides each.
Name the iterator categories and which container provides each.
Six categories: input/output (single-pass), forward (forward_list), bidirectional (list, set, map), random-access (vector, deque, array), and C++20 contiguous (vector, array, string, span).
Common mistakes
- ✗Calling
std::sorton astd::list— compile error, list iterators aren't random-access; uselist::sort - ✗Subtracting
forward_listiterators expecting O(1) — they're not random-access - ✗Treating contiguous and random-access as the same —
dequeis random-access but not contiguous
Follow-up questions
- →What does C++20
std::contiguous_iteratoradd overstd::random_access_iterator? - →How do iterator concepts (C++20) replace iterator traits?
JuniorTheoryOccasionalHow do you count elements in std::list? Why was it O(n) pre-C++11?
How do you count elements in std::list? Why was it O(n) pre-C++11?
C++11 mandates O(1) size() via a cached counter; pre-C++11 it was implementation-defined and often O(n). The trade-off: full-list splice became O(n) because the count must be updated.
Common mistakes
- ✗Assuming
list::size()is O(n) in modern code and using a manual counter — unnecessary since C++11 - ✗Forgetting that the O(1)/O(n) trade-off for splice was a real design decision, not an oversight
- ✗Using
std::distance(list.begin(), list.end())for size — always O(n) for bidirectional iterators
Follow-up questions
- →What is the time complexity of
std::list::splicein C++11 and why? - →When should you prefer
std::listoverstd::vectordespitevector's better cache performance?
JuniorTheoryOccasionalWhat's the difference between set and multiset, and when does each make sense?
What's the difference between set and multiset, and when does each make sense?
std::set stores unique keys; insert returns pair<iter, bool> with false for duplicates. std::multiset allows duplicates and always succeeds; use it when duplicates are meaningful and equal_range to walk matches.
Common mistakes
- ✗Using
multiset::erase(key)and being surprised it removes all duplicates of that key - ✗Iterating with
find(k)and++itto walk equal keys — works butequal_rangeis clearer - ✗Storing custom objects without a strict weak ordering — UB
Follow-up questions
- →How does
set::erase(iter)differ fromset::erase(key)in return value? - →What is the complexity of
multiset::count(k)and why?
MiddleTheoryOccasionalHow do you extend STL containers with custom allocators or policies?
How do you extend STL containers with custom allocators or policies?
Pass a custom allocator as the container's template parameter — it must provide value_type, allocate, deallocate (or use allocator_traits defaults). C++17 std::pmr offers polymorphic memory resources without re-templating.
Common mistakes
- ✗Forgetting the
rebindmechanism — allocators for node-based containers may be rebound to a different type internally - ✗Not making the allocator stateless when using it with
std::vector— stateful allocators affect copy/move semantics - ✗Implementing
allocatewithout checking forn == 0— undefined behaviour in some allocator requirements
Follow-up questions
- →What is the difference between
std::pmr::monotonic_buffer_resourceandstd::pmr::pool_options? - →How do you use
std::pmr::vectorwith a stack-based buffer for temporary allocations?
MiddleTheoryOccasionalWhat must a class implement to be a valid C++ iterator?
What must a class implement to be a valid C++ iterator?
A ForwardIterator must provide operator*, prefix operator++, operator==/!=, a copy constructor, plus the type aliases (iterator_category, value_type, difference_type, pointer, reference) via nested types or iterator_traits.
Common mistakes
- ✗Forgetting to provide
iterator_category— algorithms silently fall back to the most conservative behaviour - ✗Not implementing postfix
operator++— some algorithms and ranged-for need it - ✗Not making the iterator satisfy the equality comparable requirement —
operator==must be consistent withoperator!=
Follow-up questions
- →How do C++20
std::sentinel_forandstd::sized_sentinel_forimprove range termination over matching begin/end types? - →What is the difference between
iteratorandconst_iteratorand how do you provide both?
MiddleTheoryOccasionalWhat does shrink_to_fit guarantee and when would you use it?
What does shrink_to_fit guarantee and when would you use it?
shrink_to_fit() is a non-binding request to release unused capacity — implementations may ignore it. The portable fallback is the swap idiom: std::vector<T>(v).swap(v).
Common mistakes
- ✗Calling
shrink_to_fitafter every operation — fights against amortised growth - ✗Expecting a hard guarantee on capacity — implementation may ignore the call
- ✗Doing it on tiny containers where the cost of the copy outweighs the freed bytes
Follow-up questions
- →Why is the swap idiom needed in addition to
shrink_to_fit? - →Does
std::deque::shrink_to_fitexist and what does it do?
MiddleTheoryOccasionalWhat is std::span and when should it replace T* + size_t parameters?
What is std::span and when should it replace T* + size_t parameters?
std::span<T> (C++20) is a non-owning pointer+length view over any contiguous range. Use it as a function parameter to accept vector, array, or C arrays uniformly without templates; the buffer must outlive the span.
Common mistakes
- ✗Returning a
spanto a localvector— dangling reference - ✗Storing a
spanmember in a long-lived object whose source data changes — invalidated on reallocation - ✗Confusing
span<T>withspan<const T>— the former allows modification through the view
Follow-up questions
- →How does
std::spandiffer fromgsl::spanfrom the GSL library? - →What is a static-extent span (
std::span<T, N>) used for?
MiddleTheoryOccasionalHow does std::string differ from std::vector<char> in implementation and behaviour?
How does std::string differ from std::vector<char> in implementation and behaviour?
Both are contiguous, but std::string adds a guaranteed null terminator (c_str() is O(1)), small-string optimisation, text APIs (find, substr), and char_traits. vector<char> has none of these — use string for text.
Common mistakes
- ✗Using
vector<char>for text and then needing to manually null-terminate for C APIs - ✗Assuming
string::data()is null-terminated only since C++11 (older standards didn't guarantee it) - ✗Using
stringto store binary blobs — works, butvector<std::byte>is clearer about intent
Follow-up questions
- →What is
char_traitsand how doesstd::wstringuse it? - →Why is
std::basic_stringa class template?
SeniorTheoryOccasionalWhat is the role of an allocator in STL containers and when do you write a custom one?
What is the role of an allocator in STL containers and when do you write a custom one?
The Allocator template parameter controls a container's memory source via allocate/deallocate. Write a custom one for arena/pool/NUMA/instrumentation; C++17 std::pmr::polymorphic_allocator swaps behaviour at runtime.
Common mistakes
- ✗Forgetting that two containers with different allocators have different types and don't compare/swap by default
- ✗Writing a stateful allocator and not handling propagation traits correctly
- ✗Comparing performance without the workload — pool allocators win for many small allocations, lose for big ones
Follow-up questions
- →What is
propagate_on_container_copy_assignmentand when does it matter? - →How does
std::pmr::vectorinteroperate withstd::vector?
SeniorTheoryOccasionalInternals of std::set, std::map, std::unordered_map, and std::hash.
Internals of std::set, std::map, std::unordered_map, and std::hash.
std::set/std::map are red-black trees: O(log n) ops, stable iterators on insert. std::unordered_map is a separate-chaining hash table: O(1) average, O(n) worst case if std::hash collides badly.
Common mistakes
- ✗Implementing
std::hashby XORing all fields — produces many collisions for keys that differ only slightly - ✗Modifying a key in-place inside a
std::setorstd::mapviaconst_cast— breaks the BST invariant silently - ✗Assuming pointer stability for
unordered_mapafter rehash — references/pointers to values are invalidated
Follow-up questions
- →What is the difference between
std::set::insertandstd::set::emplacein terms of performance? - →How does
std::unordered_map::reservediffer fromrehash?
SeniorPerformanceRareWhat is std::flat_map (C++23) and when does it beat std::map?
What is std::flat_map (C++23) and when does it beat std::map?
std::flat_map<K, V> (C++23) is a sorted contiguous container (two parallel vectors): O(log n) binary-search lookup with cache locality beating std::map, but O(n) insert/erase. Best for read-heavy workloads.
Common mistakes
- ✗Using
flat_mapfor write-heavy workloads — O(n) inserts ruin performance - ✗Expecting iterator stability like
std::map— flat_map has none - ✗Forgetting that range constructors must sort or accept pre-sorted input via
sorted_unique_ttag
Follow-up questions
- →How does
flat_mapinteract with allocators? - →What is
flat_setand when does it replaceset?
SeniorPerformanceRareWhat is small-buffer optimisation in containers and why use boost::small_vector?
What is small-buffer optimisation in containers and why use boost::small_vector?
SBO embeds a fixed inline buffer of N elements; while size() ≤ N no heap allocation occurs, above it falls back to dynamic. Trade-off: skips heap for small sizes but sizeof(container) grows by N * sizeof(T).
Common mistakes
- ✗Choosing N too large — wastes stack space and grows objects passed by value
- ✗Returning
small_vector::data()from a function and storing it across operations — pointer changes when crossing the inline/heap boundary - ✗Assuming
std::vectorhas SBO — the standard does not provide it
Follow-up questions
- →How does
std::string's SSO interact with move semantics? - →Why doesn't
std::vectorhave SBO in the standard?