vector, deque, list, forward_list. How data structure choice shapes performance, and why iterator invalidation is the trap that gets everyone.
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.
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.
The standard library defines six sequential containers. Each makes a different trade-off between access speed and insertion/deletion speed:
| Container | Storage | Random Access | Insert/Delete Front | Insert/Delete Back | Insert/Delete Mid |
|---|---|---|---|---|---|
vector | Contiguous | O(1) | O(n) | Amortized O(1) | O(n) |
deque | Paged | O(1) | O(1) | O(1) | O(n) |
list | Doubly linked | O(n) | O(1) | O(1) | O(1) |
forward_list | Singly linked | O(n) | O(1) | O(n) | O(1) |
array | Contiguous, fixed | O(1) | N/A | N/A | N/A |
string | Contiguous | O(1) | O(n) | Amortized O(1) | O(n) |
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.All sequential containers share a core set of operations. Learning these once means you can use any container:
| Operation | Effect |
|---|---|
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_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)).emplace_back over push_back?The cost of insertion and deletion depends entirely on the data structure. Let's look at what actually happens in memory:
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.
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).
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).
vector slow?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).
| Property | Meaning |
|---|---|
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) |
v.reserve(n) before inserting. This eliminates all intermediate reallocations and can be a massive performance win for large vectors.Push elements and watch size (warm) vs capacity (teal). Each reallocation doubles capacity and copies all elements.
v.reserve(100) do?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.
| Operation | vector | deque | list |
|---|---|---|---|
| push_back | If reallocation: ALL. Otherwise: only end() | ALL invalidated | None |
| push_front | N/A | ALL invalidated | None |
| insert | If reallocation: ALL. Otherwise: at/after point | ALL invalidated | None |
| erase | At/after erased element | ALL (unless front/back) | Only the erased element |
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
vector reallocates due to push_back, which iterators remain valid?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.
Select a container type and perform operations. Watch how the internal memory layout changes.
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.
| Adaptor | Semantics | Default Container | Key Operations |
|---|---|---|---|
stack | LIFO (last in, first out) | deque | push, pop, top |
queue | FIFO (first in, first out) | deque | push, pop, front, back |
priority_queue | Highest priority first | vector | push, 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
stack use by default?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 Chapter | Builds Toward |
|---|---|
| Container operations | Generic algorithms (Ch 10) that work on any container |
| Iterator invalidation | Safe coding with move semantics (Ch 13) |
| vector growth | Dynamic memory and allocators (Ch 12) |
| deque internals | Stack/queue adaptors for algorithms |
| Container choice | Profiling and optimization in real systems |
Next up: Chapter 10: Generic Algorithms — powerful operations that work on any container through iterators.