C++ STL Containers: vector, map, set, unordered_map
Pick the right C++ STL container for the job: vector, deque, list, map, set, unordered_map, and unordered_set with their complexity guarantees.
What you'll learn
- ✓Choose vector, deque, list, or array correctly
- ✓Use map and set for ordered data
- ✓Reach for unordered_map and unordered_set for fast lookup
- ✓Apply emplace, reserve, and structured iteration
- ✓Read complexity guarantees before choosing a container
Prerequisites
- •Templates basics — see /blog/cpp-templates-basics
The Standard Template Library is C++‘s great strength. Pick the right container and your code is both fast and short. Pick the wrong one and you fight the data structure forever. The choice is almost always driven by access pattern, not by what feels natural.
Sequence containers
These store elements in a defined order.
std::vector— contiguous, dynamic-size array. The default sequence container.std::array— fixed-size, stack-allocated. Use when the size is a compile-time constant.std::deque— double-ended queue with fast push/pop at both ends.std::list— doubly linked list. Almost never the right choice.std::forward_list— singly linked. Even more niche.
Default to std::vector. Modern CPUs love contiguous memory; cache locality usually beats any algorithmic edge a list has.
vector basics
#include <vector>
std::vector<int> v{1, 2, 3};
v.push_back(4);
v.emplace_back(5); // constructs in place
for (int x : v) std::cout << x << ' ';
std::cout << v.size() << ' ' << v.front() << ' ' << v.back();
emplace_back forwards its arguments to the element constructor, saving a copy when the element type is non-trivial.
Reserve when you know the size
push_back may reallocate and copy when capacity runs out. If you know roughly how many elements are coming, call reserve first.
std::vector<int> v;
v.reserve(1'000'000);
for (int i = 0; i < 1'000'000; ++i) v.push_back(i);
This turns many reallocations into one.
Associative containers (ordered)
std::map and std::set are red-black trees: ordered, with O(log n) insert, lookup, and erase.
#include <map>
#include <set>
std::map<std::string, int> ages{{"ada", 36}, {"alan", 41}};
ages["grace"] = 60;
if (auto it = ages.find("ada"); it != ages.end()) {
std::cout << it->second;
}
std::set<int> seen{1, 2, 3};
seen.insert(4);
Use the ordered versions when you need range queries, sorted iteration, or lower_bound/upper_bound.
Unordered associative containers
std::unordered_map and std::unordered_set are hash tables with average O(1) operations.
#include <unordered_map>
std::unordered_map<std::string, int> counts;
++counts["apple"];
++counts["apple"];
++counts["pear"];
Use the unordered versions when you only need fast lookup and do not care about order. Worst case is O(n) if all keys hash to the same bucket; for hostile inputs (web requests), consider randomized hashing.
emplace vs insert
insert takes an already-constructed element. emplace forwards its arguments to the element constructor.
std::map<std::string, std::vector<int>> by_name;
by_name.insert({"a", {1, 2}});
by_name.emplace("b", std::vector<int>{3, 4});
For map and set, emplace constructs the pair in place, saving one copy.
Iteration patterns
Range-for handles 95% of cases:
for (const auto& [key, value] : ages) {
std::cout << key << " -> " << value << '\n';
}
The structured binding [key, value] decomposes each pair. See Modern C++ Features for more.
For index-based access, use .size() with std::size_t:
for (std::size_t i = 0; i < v.size(); ++i) std::cout << v[i];
v[i] does no bounds check; v.at(i) does and throws on out-of-range.
Removing elements
The erase-remove idiom moves unwanted elements to the end and erases them. C++20 simplifies it with std::erase and std::erase_if.
#include <vector>
std::vector<int> v{1, 2, 3, 4, 5};
std::erase_if(v, [](int x) { return x % 2 == 0; }); // removes 2, 4
For maps and sets, prefer erase directly on iterators or keys.
Complexity at a glance
vector: random accessO(1), push_back amortizedO(1), middle insertO(n).deque: random accessO(1), push/pop at either endO(1).map/set: insert, find, eraseO(log n), ordered iteration.unordered_map/unordered_set: insert, find, erase averageO(1).
Whenever you reach for a container, glance at this table. It will save you from list mistakes.
A worked example
Counting words in input, sorted by frequency:
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>
#include <algorithm>
int main() {
std::unordered_map<std::string, int> counts;
for (std::string w; std::cin >> w; ) ++counts[w];
std::vector<std::pair<std::string, int>> sorted(counts.begin(), counts.end());
std::sort(sorted.begin(), sorted.end(),
[](const auto& a, const auto& b) { return a.second > b.second; });
for (const auto& [word, n] : sorted) {
std::cout << word << ' ' << n << '\n';
}
}
Fast counting via the hash map, then a single sort for ordering.
Common pitfalls
- Using
std::listbecause “insertion is O(1)” — pointer chasing usually loses. - Forgetting
reservebefore a big batch ofpush_back. - Iterating with
intinstead ofstd::size_tand triggering signed/unsigned warnings. - Reading
m[key]for membership test — it inserts a default value if missing.
What is next
Read Smart Pointers to put owning pointers in containers safely. Then Modern C++ Features for auto, range-for, and structured bindings used heavily here.
Wrap up
std::vector is the right answer to most container questions. Reach for ordered associative containers when sort order matters, and the unordered versions when only lookup speed does. Always look at complexity before reaching for the exotic. The STL pays back time spent learning it many times over.