2Brute2Best
πŸ”₯0
M5.1

Sequence Containers (vector, deque, list, forward_list, array)

Medium High-frequency interview topic Pro Onlystd::vector Growthstd::deque Chunk Mapstd::list vs forward_listreserve vs resizevector<bool> Trap
⚑ 15-Second Mental ModelM5.1 · Instant Intuition
Single-Read Mastery
The Real-World Physical Analogy (std::vector Internals)

std::vector is a train that doubles its carriage count every time it fills up. Appending is instant 99% of the time, but when full, it buys a brand new double-sized track and moves all passengers over in one go.

Rule to Remember: Always call vec.reserve(N) if you know the count upfront to skip geometric reallocations.
3 Golden Facts to Remember
Fact #1
What are the exact data members inside a typical 64-bit std::vector, and why is sizeof(std::vector<T>) equal to 24 bytes?

On 64-bit platforms (GCC libstdc++, Clang libc++, MSVC STL), std::vector<T> contains exactly three raw 8-byte pointers: T* _M_start (points to the first allocated element), T* _M_finish (points one past the last valid element), and T* _M_end_of_storage (points one past the end of the allocated capacity). $3 Γ— 8 = 24$ bytes. size() is computed on the fly as _M_finish - _M_start, and capacity() is _M_end_of_storage - _M_start.

Fact #2
Why does std::deque support both O(1) push_front and O(1) push_back without invalidating pointers to existing elements, while std::vector cannot?

std::vector stores all elements in a single contiguous block of heap memory; expanding capacity requires allocating a larger block and migrating all elements, which invalidates all existing pointers. In contrast, std::deque stores elements in a segmented collection of fixed-size chunks indexed by a central pointer map. Pushing to the front or back simply allocates a new fixed-size chunk and adds its pointer to the map. Because the existing chunks remain at their original heap addresses, pointers and references to existing elements never move and are never invalidated.

Fact #3
What is the difference between reserve() and resize() in std::vector, and what happens to iterators in each?

reserve(N): Pre-allocates raw memory so capacity() >= N. It does not construct any objects and does not change size(). Iterators are invalidated if and only if $N > capacity$ (triggering a reallocation).

  • resize(N): Modifies size() to equal N. If $N > size(), it default-constructs or copy-constructs N - size() new elements. If N < size()$, it destroys excess elements from the back. Like reserve, it reallocates and invalidates iterators if $N > capacity$.
01

Interviewer's lens

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

Interviewers probe sequence containers to quickly distinguish between textbook memorizers and engineers who understand physical computer hardware:

  • Textbook trap: Weak candidates quote Big-O in a vacuum (e.g. claiming std::list is faster because insertion is 'always O(1)').
  • Hardware reality: Senior candidates know that contiguous L1/L2 cache line prefetching makes std::vector dramatically outperform node-based lists in almost every real workload.
  • Key metrics expected: The exact 24-byte 3-pointer stack layout of std::vector, how geometric growth factors (1.5x vs 2.0x) amortize allocation cost, why std::deque's chunk-map gives pointer stability across pushes, and how to avoid the infamous std::vector<bool> bitfield proxy reference trap in production.
02

Under the hood & memory layout

Hardware & memory representation

Think of sequence containers as three different physical storage systems:

  • std::vector is a single expanding bench: Everyone sits shoulder-to-shoulder in one unbroken row. Its 24-byte stack frame holds three pointers: begin, end, and end_of_storage. When the bench is full, it builds a brand new bench twice as long (2.0x on GCC/Clang, 1.5x on MSVC), moves everyone over, and tears down the old one.
  • std::deque is a ring binder of paper pages: Instead of one giant page, it keeps an index of fixed-size chunks (typically 512 bytes). Adding at the front or back simply snaps in a new page into the binderβ€”existing seats never move, so pointers and references stay permanently valid.
  • std::list is a scavenger hunt of sticky notes: Every single item is a separate heap allocation holding prev and next pointers (24 bytes of overhead on 64-bit for just one value). Traversing it forces the CPU to chase pointers across scattered memory, destroying L1/L2 cache prefetching.
  • std::array is a fixed egg carton: Sits directly on the stack with zero heap allocation and zero memory overhead.
std::vector<T> Stack Header (24 Bytes)

Consists of three 64-bit pointers: T* _M_start (first element), T* _M_finish (one past last element, size() = _M_finish - _M_start), and T* _M_end_of_storage (capacity() = _M_end_of_storage - _M_start).

std::deque<T> Chunk-Map (40–48 Bytes Stack)

Stores a pointer-to-pointer map T** _M_map, a map size size_t, and two iterators (_M_start and _M_finish) each containing pointers to current element, first element of chunk, last element of chunk, and node pointer in map. Elements live in fixed-size buffers (typically 512 bytes).

std::list<T> Doubly Linked Node (24B Overhead + sizeof(T))

Every element is wrapped in a distinct heap node: void* _M_next (8B), void* _M_prev (8B), plus alignment padding (8B) and T payload. Traversing nodes forces non-contiguous pointer chasing that defeats CPU L1/L2 prefetchers.

std::vector<bool> Bit-Packed Specialization

Does not store bool (1 byte each). Instead, packs 8 boolean flags into 1 byte (or 64 per uint64_t word). Indexing v[i] returns an ephemeral std::vector<bool>::reference proxy object instead of bool&.

03

Core concepts & syntax traps

01

std::vector Internal Memory, Geometric Growth & Amortized O(1)

Imagine packing a suitcase that cannot stretch: once full, you must buy a larger suitcase, move every shirt over, and throw the old one away.

When push_back() exceeds capacity(), std::vector runs a 3-step geometric migration:

  1. Allocate: Requests a brand new contiguous heap block scaled by a growth factor (2.0x in GCC libstdc++ and Clang libc++, 1.5x in MSVC STL).
  2. Migrate: Transfers elements to the new block using std::move_if_noexcept (falling back to copying if the move constructor might throw, maintaining strong exception safety).
  3. Deallocate: Destroys the old elements and frees the old heap buffer, immediately dangling all previous iterators and pointers.

Amortized vs Worst-Case Latency:

  • Amortized O(1): Because capacity grows geometrically, reallocations become exponentially rarer. Populating N elements requires roughly 2N total writes across all expansions.
  • Worst-case O(N) spike: The single push_back() triggering reallocation halts execution to copy memory. Eliminate this latency spike upfront with reserve(N).
C++23 snippet
std::vector<int> v;
v.reserve(1000); // Allocates capacity for 1000 elements upfront without constructing them!
for (int i = 0; i < 1000; ++i) {
    v.push_back(i); // Guaranteed ZERO reallocations and ZERO pointer invalidations
}
v.shrink_to_fit(); // Non-binding request to shrink capacity() down to match size()
Interview takeaway:

Always call reserve() when the final element count or an upper bound is known in advance.

02

reserve() vs resize() & The Default Construction Trap

Candidates often mix up reserve() and resize(), causing subtle crashes or wasted allocations:

  • reserve(n) reserves raw land without building houses:

    • Affects capacity() only; size() remains strictly 0.
    • Allocates raw heap bytes for at least n elements without running constructors.
    • The trap: Calling v.reserve(10) then accessing v[0] = 42 is Undefined Behavior because no element exists yet! Use push_back() instead.
  • resize(n) actually constructs the houses:

    • Modifies size() directly.
    • If n > size(), it appends n - size() new value-initialized elements (e.g. 0 for int), invoking constructors.
    • If n < size(), it truncates the vector by destroying excess elements from the back, leaving capacity() unchanged.
C++23 snippet
std::vector<int> v1;
v1.reserve(5); // size=0, capacity=5. v1[0] is UNDEFINED BEHAVIOR!

std::vector<int> v2;
v2.resize(5);  // size=5, capacity=5. Elements are initialized to 0: {0, 0, 0, 0, 0}
v2[0] = 42;    // Perfectly legal
Interview takeaway:

Use reserve() when you plan to append elements with push_back()/emplace_back(); use resize() when you want to immediately index elements with operator[].

03

std::deque: Chunk Map Architecture & Pointer Invariance

Instead of one long continuous bench like std::vector, std::deque is built like a train with fixed-size cars (chunks of 512 bytes in libstdc++):

  1. Chunk-Map Index: A central pointer table tracks each fixed-size page buffer scattered in heap memory.
  2. Stable Seat Addresses (Pointer Invariance): Pushing to the front or back simply allocates a new 512-byte chunk and attaches it to the map. Existing chunks never move in memory, meaning pointers and references to elements remain permanently valid!
  3. Double-Hop Indexing: Accessing dq[i] runs in O(1) time using two pointer dereferences instead of one:
    • Chunk index: chunk = i / CHUNK_ELEMS
    • Element offset: offset = i % CHUNK_ELEMS
  4. Iterator Invalidation Nuance: While element references never die on front/back pushes, iterators can invalidate if the central pointer table expands.
C++23 snippet
std::deque<int> dq = {1, 2, 3};
const int* ptr = &dq[1]; // Points to '2'

dq.push_front(0); // Allocates front chunk if needed; DOES NOT move existing chunks!
dq.push_back(4);  // Allocates back chunk if needed

// ptr is STILL 100% valid and still points to 2! (Vector would have dangled here)
assert(*ptr == 2);
Interview takeaway:

Choose std::deque over std::vector when you need O(1) push_front or when pointers to elements must survive insertions at both ends.

04

std::list vs std::forward_list: Splicing & Memory Overhead

Linked lists decouple storage completely: every element lives inside its own isolated heap box.

The Splicing Superpower (O(1) Node Adoption):

  • std::list::splice() transfers entire node chains between lists in strictly O(1) time.
  • It only rewires boundary prev and next pointers. Zero heap allocations occur, zero elements are copied, and existing node memory addresses are 100% preserved (ideal for LRU caches).

The Memory & Cache Penalty:

  • Node Overhead: On 64-bit systems, each std::list node needs 16 bytes for pointers (prev + next) plus alignment paddingβ€”totaling 24 bytes of overhead per element. Storing a 4-byte int wastes 600% extra memory!
  • Cache Annihilation: Nodes are scattered in RAM, turning iteration into pointer chasing that misses L1/L2 caches.
  • std::forward_list (Singly-Linked): Cuts overhead down to 8 bytes by dropping prev. It deliberately omits .size() to prevent developers from accidentally running an O(N) count in hot loops.
C++23 snippet
std::list<int> a = {1, 2};
std::list<int> b = {3, 4};
// Transfers all nodes of 'b' into 'a' at a.begin() in O(1) without any allocation!
a.splice(a.begin(), b); 
// a is now {3, 4, 1, 2}, b is empty. Pointers to nodes 3 and 4 are UNCHANGED.
Interview takeaway:

Use std::list only when you need O(1) splice or zero-invalidation node insertions; for forward-only singly-linked chains with no size overhead, use std::forward_list.

05

The std::vector<bool> Proxy Reference Trap & Thread Safety

In an attempt to conserve memory, C++ specialized std::vector<bool> to pack 8 boolean flags into each byte. This well-intentioned optimization breaks core language contracts:

  1. No True bool& Exists: Hardware can only address whole bytes, never individual bits. Because the compiler cannot form a raw pointer or reference to a bit, operator[] returns a temporary proxy object (std::vector<bool>::reference).
  2. auto& Fails Compilation: Writing auto& flag = v[0]; fails to compile because a non-const reference bool& cannot bind to an ephemeral proxy object.
  3. Template Breakage: Generic templates expecting T& ref = container[i]; break when instantiated with bool.
  4. Multithreaded Data Races: Writing to v[0] and v[1] from separate threads simultaneously triggers a data race. The CPU cannot write a single bit in isolation; it issues a read-modify-write on the entire shared machine word!

The Production Fix: Use std::vector<uint8_t> for byte-addressable, thread-isolated booleans, or std::bitset<N> when sizes are known at compile time.

C++23 snippet
std::vector<bool> vb = {true, false};
// auto& bad = vb[0]; // COMPILE ERROR: cannot bind bool& to temporary proxy!
auto good = vb[0];   // Warning: 'good' is std::vector<bool>::reference, NOT bool!

// Clean production fix when byte-addressability is needed:
std::vector<uint8_t> safe_bools = {1, 0};
auto& ref = safe_bools[0]; // Real uint8_t&, completely safe and thread-isolated!
Interview takeaway:

Never use std::vector<bool> in generic code or multithreaded pipelines; use std::vector<uint8_t> or std::bitset instead.

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