C++ Primer, Chapter 9

Sequential Containers

vector, deque, list, forward_list. How data structure choice shapes performance, and why iterator invalidation is the trap that gets everyone.

Prerequisites: Chapter 3 (Strings & Vectors) + Chapter 7 (Classes).
9
Chapters
5+
Simulations

Chapter 0: Why Sequential Containers?

You need a list of names. Easy — use a vector<string>. But what if you frequently insert names at the front? Or in the middle? Or need to process them in FIFO order? The same container that's fast for one pattern is slow for another.

The C++ standard library provides several sequential containers, each with different performance trade-offs. They all store elements in a programmer-specified order (not sorted by value), but they differ in how they store elements internally — and that determines which operations are fast or slow.

The core insight: There is no universally best container. The right choice depends on your access pattern. The library gives you the tools; understanding the trade-offs is your job.
Container Operations Race

Click an operation and see which container wins. push_front is fast for deque/list but impossible for vector. Random access is fast for vector/deque but slow for list.

Why does the standard library provide multiple sequential container types?

Chapter 1: The Zoo — Container Types

The standard library defines six sequential containers. Each makes a different trade-off between access speed and insertion/deletion speed:

ContainerStorageRandom AccessInsert/Delete FrontInsert/Delete BackInsert/Delete Mid
vectorContiguousO(1)O(n)Amortized O(1)O(n)
dequePagedO(1)O(1)O(1)O(n)
listDoubly linkedO(n)O(1)O(1)O(1)
forward_listSingly linkedO(n)O(1)O(n)O(1)
arrayContiguous, fixedO(1)N/AN/AN/A
stringContiguousO(1)O(n)Amortized O(1)O(n)
Lippman's default: Use vector unless you have a specific reason not to. It has the best cache locality (contiguous memory), the simplest interface, and fast random access. Only switch to something else when profiling shows you need to.

Choosing a Container

Need random access?
vector or deque
Insert/delete at front?
deque (not vector)
Insert/delete in middle?
list or forward_list
Fixed size, no resize?
array
Which container should you use if you need fast insertion at both front and back, plus random access?

Chapter 2: Common Operations

All sequential containers share a core set of operations. Learning these once means you can use any container:

OperationEffect
c.empty()True if no elements
c.size()Number of elements
c.push_back(v)Add to end (not for array/forward_list)
c.push_front(v)Add to front (list, deque, forward_list only)
c.insert(p, v)Insert v before iterator p
c.erase(p)Remove element at iterator p
c.front()Reference to first element
c.back()Reference to last element (not forward_list)
c.begin() / c.end()Iterator range
c.clear()Remove all elements
c++
vector<int> v = {1, 2, 3};
v.push_back(4);          // {1, 2, 3, 4}
v.insert(v.begin(), 0); // {0, 1, 2, 3, 4} — O(n) for vector
v.erase(v.begin() + 2); // {0, 1, 3, 4} — shifts elements left
emplace vs push/insert: emplace_back(args...) constructs the element in place, avoiding a copy. Use it when constructing objects directly: v.emplace_back("hello", 5) instead of v.push_back(MyType("hello", 5)).
What is the advantage of emplace_back over push_back?

Chapter 3: Insert & Erase Under the Hood

The cost of insertion and deletion depends entirely on the data structure. Let's look at what actually happens in memory:

vector insert at middle

Every element after the insertion point must be shifted right to make room. This is O(n) where n is the number of elements after the insertion point. If the vector is full, it must also reallocate.

list insert at middle

A doubly-linked list just rewires two pointers. The new node points to its neighbors, and the neighbors update their pointers. No elements are moved. O(1).

deque insert at front

A deque stores elements in blocks (pages). Inserting at the front just uses space in the first block. If the first block is full, a new block is allocated. No shifting needed. O(1).

The trade-off is real. A vector with 10 million elements takes ~10 million copies to insert at position 0. A list takes constant time. But iterating through a list is slower because nodes are scattered in memory (poor cache locality). Choose based on your dominant operation.
Why is inserting in the middle of a vector slow?

Chapter 4: How vector Grows

A vector stores elements in a single contiguous block. When that block fills up and you push_back another element, the vector must allocate a bigger block, copy everything, and free the old one. This sounds expensive — and it is, per-reallocation. But the doubling strategy makes the average cost per push O(1).

Size vs Capacity

PropertyMeaning
v.size()Number of elements currently stored
v.capacity()Number of elements that fit before reallocation
v.reserve(n)Request capacity for at least n elements (no shrink)
v.shrink_to_fit()Request to reduce capacity to match size (non-binding)
Pro tip: If you know how many elements you'll need, call v.reserve(n) before inserting. This eliminates all intermediate reallocations and can be a massive performance win for large vectors.
vector Growth Strategy

Push elements and watch size (warm) vs capacity (teal). Each reallocation doubles capacity and copies all elements.

size: 0 / cap: 0
What does v.reserve(100) do?

Chapter 5: Iterator Invalidation

This is the trap that bites every C++ programmer at least once. When you modify a container's size, existing iterators, pointers, and references to its elements may become invalid. Using an invalidated iterator is undefined behavior — crashes, garbage data, or silent corruption.

When Are Iterators Invalidated?

Operationvectordequelist
push_backIf reallocation: ALL. Otherwise: only end()ALL invalidatedNone
push_frontN/AALL invalidatedNone
insertIf reallocation: ALL. Otherwise: at/after pointALL invalidatedNone
eraseAt/after erased elementALL (unless front/back)Only the erased element
Golden rule: Never modify a container's size while iterating with a cached iterator. If you must insert or erase during a loop, use the return value of insert/erase to get a fresh, valid iterator.
c++
// CORRECT: erase returns iterator to next element
auto it = v.begin();
while (it != v.end()) {
    if (*it % 2 == 0)
        it = v.erase(it);   // erase returns next valid iterator
    else
        ++it;
}

// WRONG: it is invalidated after erase
for (auto it = v.begin(); it != v.end(); ++it)
    if (*it % 2 == 0)
        v.erase(it);  // BUG! it is now invalid, ++it is UB
After a vector reallocates due to push_back, which iterators remain valid?

Chapter 6: Container Comparison Simulator

Let's compare how vector, deque, and list actually store and manipulate data. Each uses a fundamentally different memory layout, which is why their operation costs differ.

Data Structure Internals

Select a container type and perform operations. Watch how the internal memory layout changes.

Observe the difference: When you push_front on a vector, every element shifts right (expensive). On a deque, only the first block is affected. On a list, a single new node is linked in. This is why data structure choice matters.

Chapter 7: Container Adaptors

A container adaptor wraps an existing container to provide a different interface. The adaptor restricts which operations are available, giving you a specific data structure semantics.

AdaptorSemanticsDefault ContainerKey Operations
stackLIFO (last in, first out)dequepush, pop, top
queueFIFO (first in, first out)dequepush, pop, front, back
priority_queueHighest priority firstvectorpush, pop, top
c++
#include <stack>
stack<int> stk;
stk.push(1);  stk.push(2);  stk.push(3);
stk.top();     // 3 (most recently pushed)
stk.pop();     // removes 3; top is now 2
Adaptors are restrictive by design. A stack doesn't let you iterate or access arbitrary elements. That's the point — it enforces LIFO discipline. If you need to iterate, use the underlying container directly.
What underlying container does stack use by default?

Chapter 8: Beyond — Connections

Sequential containers are the backbone of C++ programs. Choosing the right one and understanding iterator invalidation are skills that separate novice code from professional code.

This ChapterBuilds Toward
Container operationsGeneric algorithms (Ch 10) that work on any container
Iterator invalidationSafe coding with move semantics (Ch 13)
vector growthDynamic memory and allocators (Ch 12)
deque internalsStack/queue adaptors for algorithms
Container choiceProfiling and optimization in real systems
Lippman's advice: "Use vector unless there's a good reason to prefer another container." Most programs access elements far more often than they insert in the middle, and vector's cache-friendly layout wins in practice.

Continue Reading

Next up: Chapter 10: Generic Algorithms — powerful operations that work on any container through iterators.