Why std::vector Outperforms Other C++ Data Structures
std::vector is the right default choice for almost every container need in C++. It’s the foundation of high-performance applications—game engines, financial systems, data processing pipelines—because it aligns with how modern CPUs actually work.
Cache Locality Drives Performance
std::vector stores elements in contiguous memory. When you iterate, the CPU prefetches the next elements into L1/L2 cache, giving you predictable, fast access patterns. Competing containers scatter data across the heap.
Compare iteration performance:
- std::vector: Contiguous memory → cache hits → fast
- std::list: Nodes scattered across heap → cache misses → slow
- std::deque: Split into chunks → partial locality → slower than vector
A simple iteration benchmark on modern hardware shows std::vector can be 10-20x faster than std::list for the same operation, purely because of cache behavior. CPU prefetchers work best with linear access patterns.
Amortized O(1) Growth
When you push_back() to a full vector, it reallocates—doubling capacity, copying elements to the new block. This sounds expensive, but it happens rarely enough that the amortized cost per insertion is still O(1). If you insert n elements, you do O(n) total work, not O(n²).
The growth factor matters. Most standard libraries use factor 2, which gives you ~log(n) reallocations for n insertions. Some embedded or resource-constrained environments tune this differently, but 2 is the safe default.
Practical Optimization Patterns
Reserve when you know the size:
std::vector<Item> items;
items.reserve(10000); // Allocate once, no reallocations during growth
for (auto& source : data_source) {
items.push_back(source);
}
Calling reserve() once prevents repeated allocations. If you don’t know the final size, the doubling strategy still works—just less efficiently.
Reclaim memory after heavy deletion:
std::vector<Record> records = load_large_dataset();
// ... filter down to 1% of original size
records.erase(remove_if(records.begin(), records.end(), predicate), records.end());
records.shrink_to_fit(); // Return excess capacity to the OS
shrink_to_fit() triggers a reallocation to exact size. Use it when you’ve permanently reduced the dataset, not in tight loops.
Move semantics for zero-copy returns:
std::vector<Data> process_data() {
std::vector<Data> result;
result.reserve(expected_size);
// ... populate result
return result; // RVO/move elision, no copy
}
Modern compilers apply return value optimization (RVO) and move semantics automatically. Your vector escapes the function without copying.
When to Choose Something Else
std::vector isn’t always optimal. Use alternatives only when you have specific requirements:
- std::deque: When you need efficient push/pop on both ends (queues). Accept ~2x iteration cost.
- std::list: When you need O(1) erase in the middle from an iterator (rare). Iteration is slow; avoid for linear scans.
- std::unordered_map / std::map: When you need key-value lookup. Not a sequence container.
- std::string: For text (specialized vector for char with useful string operations).
Don’t use std::list or std::deque “just in case”—profiling shows vector wins in the vast majority of cases.
Modern CPU Architecture Matters
Recent CPUs (Zen 5, Sapphire Rapids, ARM’s latest) have larger caches and better prefetchers, making contiguous memory even more important. Speculative execution, SIMD operations, and vectorization instructions all favor the linear access patterns std::vector provides.
The old wisdom remains: Bjarne Stroustrup’s recommendation to “use vector by default, another container only with specific reason not to” is more relevant in 2026 than it was 20 years ago.
