Question
std::map vs std::unordered_map in C++: Choosing the Right Associative Container
Question
For key types such as int and std::string, where standard hash functions are available, when is there a practical reason to choose std::map instead of std::unordered_map?
std::unordered_map provides average-case constant-time lookup, while std::map provides logarithmic-time lookup. Apart from the fact that std::map keeps keys sorted, are there important differences involving performance, memory use, iteration, reliability, or supported operations? Assume the container holds a non-trivial number of elements, such as more than 1,024 keys.
Short Answer
By the end of this page, you will know that std::unordered_map is not a universal replacement for std::map. You will be able to choose between them based on ordering needs, complexity guarantees, range queries, iteration behavior, memory, key requirements, and practical performance.
Concept
std::map and std::unordered_map are both associative containers: they store values by unique keys.
The main difference is how they organize data:
std::mapis usually implemented as a balanced search tree. Its keys are kept in sorted order according to a comparison function, normallystd::less<Key>.std::unordered_mapis a hash table. A hash function assigns each key to a bucket, and elements are not stored in sorted key order.
This leads to different complexity guarantees:
| Operation | std::map | std::unordered_map |
|---|---|---|
| Lookup, insertion, erase by key | O(log n) guaranteed | Average O(1), worst-case O(n) |
| Iteration order | Sorted by key | Unspecified |
Range lookup with lower_bound / upper_bound | O(log n) | Not supported by key order |
Average O(1) is often excellent, especially for direct key lookups. But it is not the only property that matters. A map can be the correct choice when sorted traversal, range operations, predictable worst-case performance, stable iteration behavior, or a comparison-only key type is more valuable than average lookup speed.
Also, asymptotic notation describes growth as n becomes large; it does not promise that one container is faster in every real workload. Hashing strings, allocating buckets, cache behavior, collisions, and the mix of operations all affect measured performance.
Mental Model
Think of std::map as a carefully alphabetized filing cabinet.
- You can find a file by repeatedly choosing the correct half of the cabinet.
- Files are always in alphabetical order.
- You can easily ask for every file from
"m"through"r".
Think of std::unordered_map as a set of labeled bins.
- A machine hashes a key and tells you which bin to inspect.
- Finding one known key is usually very quick.
- The bins are not alphabetized, so asking for keys between two values has no useful meaning.
- If many keys land in the same bin, looking through that bin becomes slower.
Use the alphabetized cabinet when order and predictable behavior matter. Use bins when fast direct lookup is the main job.
Syntax and Examples
Both containers have similar basic APIs, including find, contains (C++20), insert, erase, operator[], and at.
#include <iostream>
#include <map>
#include <string>
#include <unordered_map>
int main() {
std::map<int, std::string> ordered{
{30, "thirty"},
{10, "ten"},
{20, "twenty"}
};
std::unordered_map<int, std::string> hashed{
{30, "thirty"},
{10, "ten"},
{20, "twenty"}
};
std::cout << "map: ";
for (const auto& [key, value] : ordered) {
std::cout << key << ' ';
}
std::cout << "\n";
std::cout << "unordered_map: ";
for (const auto& [key, value] : hashed) {
std::cout << key << ' ';
}
std::cout << "\n";
}
The map loop always prints keys in ascending order:
map: 10 20 30
The unordered_map loop can print keys in any order. You must not write code that depends on its observed iteration order.
A major map feature is ordered searching:
std::map<int, std::string> scores{
{10, "bronze"},
{50, "silver"},
{100, "gold"}
};
auto it = scores.lower_bound(50); // First key that is >= 50.
if (it != scores.end()) {
std::cout << it->first << ": " << it->second << '\n';
}
std::unordered_map cannot provide lower_bound by key because its keys are not ordered.
Step by Step Execution
Consider a lookup followed by a range query:
#include <map>
#include <iostream>
int main() {
std::map<int, char> grades{{70, 'C'}, {80, 'B'}, {90, 'A'}};
auto exact = grades.find(80);
auto passing = grades.lower_bound(60);
std::cout << exact->second << '\n';
for (auto it = passing; it != grades.end(); ++it) {
std::cout << it->first << ": " << it->second << '\n';
}
}
Execution:
gradesstores the pairs ordered by key:70,80, then90.find(80)navigates the tree to find the element whose key is exactly80.exact->secondis'B', so the first line printsB.lower_bound(60)returns the first element with a key not less than60. That is the key70.- Incrementing a
mapiterator moves to the next greater key. - The loop prints all entries with keys at least
60:
70: C
80: B
90: A
With unordered_map, find(80) is available, but step 4 has no equivalent ordered operation. You would need to inspect every element and filter keys manually.
Real World Use Cases
Choose std::map when sorted keys are part of the requirement:
- Time-based events: Store events by timestamp and retrieve the next event at or after a given time with
lower_bound. - Price levels in a trading or booking system: Iterate prices from lowest to highest, or find the first price meeting a threshold.
- Prefix-like and interval-like key processing: With sorted strings, iterate from a lower boundary to an upper boundary. For example, process names beginning with a prefix by finding a starting point and stopping when the prefix no longer matches.
- Deterministic reports and serialized output: Iterate records in a defined key order without separately sorting keys.
- Untrusted input or latency-sensitive paths: Prefer the logarithmic worst-case bound when a hash table's collision behavior is unacceptable.
- Keys without a sensible hash: A custom type only needs a strict ordering for
std::map; it needs a correct hash function and equality relation forstd::unordered_map.
Choose std::unordered_map when the work is mainly exact lookup by key:
- Caches keyed by IDs or strings.
- Counting frequencies in input data.
- Looking up configuration entries by name.
- Indexing objects by database ID.
- Fast membership tests such as
seen.contains(userId)in C++20.
Real Codebase Usage
In real code, container choice follows the operations the code must perform, not just the lookup complexity shown in a table.
Exact lookup with an unordered map
A cache commonly needs only direct access by ID:
std::unordered_map<int, User> usersById;
const User* findUser(const std::unordered_map<int, User>& users, int id) {
auto it = users.find(id);
if (it == users.end()) {
return nullptr; // Guard clause for a missing user.
}
return &it->second;
}
Ordered range processing with a map
A scheduler can use lower_bound to avoid scanning old events:
std::map<int, Task> tasksByTime;
void runTasksFrom(std::map<int, Task>& tasks, int currentTime) {
for (auto it = tasks.lower_bound(currentTime); it != tasks.end(); ++it) {
it->second.run();
}
}
Reserving an unordered map
If you know approximately how many elements will be inserted, reserve space. This can reduce costly rehashes and iterator invalidation during insertion.
std::unordered_map<std::string, int> counts;
counts.reserve(10'000);
for (const std::string& word : words) {
++counts[word];
}
Separating lookup from presentation
A frequent pattern is to use an unordered_map for fast lookup, then sort only when output is needed:
std::vector<std::pair<std::string, int>> entries(counts.begin(), counts.end());
std::sort(entries.begin(), entries.end());
This is often better than maintaining sorted order continuously if sorted output is rare. Conversely, if range queries or ordered traversal are central to the program, std::map keeps that order available all the time.
Common Mistakes
Assuming unordered_map iteration is stable or sorted
This code is incorrect if it expects ascending IDs:
std::unordered_map<int, std::string> names;
// ... insert values ...
for (const auto& [id, name] : names) {
// Incorrect assumption: id increases on every iteration.
}
Use std::map, or copy entries to a vector and sort them before ordered processing.
Treating average O(1) as guaranteed O(1)
unordered_map can have collisions. Its worst-case lookup, insertion, and erase by key are linear in the number of elements. Do not use it solely because it has a better average-case Big-O label.
Using operator[] only to check whether a key exists
operator[] inserts a missing key with a default value:
std::unordered_map<std::string, int> visits;
if (visits["alice"] == 0) { // Inserts "alice" when it was absent.
// ...
}
Use find or contains when you do not want insertion:
if (!visits.contains("alice")) {
// No insertion occurred.
}
Keeping unordered-map iterators across a rehash
Insertion can cause an unordered_map to rehash, which invalidates its iterators. Do not save an iterator and assume it remains valid after arbitrary inserts. Call reserve when a known insertion batch is coming, or reacquire the iterator.
Forgetting the hash and equality contract
For an unordered_map, equivalent keys must produce the same hash value. If custom KeyEqual considers two keys equal but the hash function returns different values for them, lookups can fail in surprising ways.
Assuming a map is always more memory-efficient
There is no universal answer. Tree nodes often have pointer and allocation overhead; hash tables have a bucket array plus stored nodes or elements. Measure with your implementation, key/value types, load factor, and expected size.
Choosing based only on element count
A container with more than 1,024 elements is not automatically an unordered_map use case. A thousand elements with frequent range queries strongly favors map; millions of exact integer-ID lookups often favor unordered_map. Benchmark representative workloads when performance matters.
Comparisons
| Property | std::map | std::unordered_map |
|---|---|---|
| Internal organization | Balanced ordered tree, typically | Hash table |
| Key order | Sorted by comparator | Unspecified |
| Key requirement | Comparable with a strict weak ordering | Hashable and equality-comparable |
find(key) | Guaranteed O(log n) | Average O(1), worst O(n) |
| Insert / erase by key | Guaranteed O(log n) | Average O(1), worst O(n) |
lower_bound, upper_bound, equal_range by order | Available and meaningful | No ordered-key equivalent |
| Iterate all keys in sorted order | Yes | No |
| Iterator invalidation on insertion | Existing iterators remain valid | Rehash invalidates iterators |
| Rehash / bucket tuning | Not applicable | Supports reserve, rehash, and load-factor settings |
| Typical best fit | Ordered traversal and ranges | Fast exact-key lookup |
std::map versus sorting a vector
For mostly-read data that changes rarely, a sorted std::vector<std::pair<Key, Value>> can also be worth considering. It offers compact contiguous storage and binary search, but inserting in the middle costs O(n). map is more suitable when you need ongoing inserts and erases while keeping keys ordered.
std::unordered_map versus std::map for strings
Hashing a long std::string generally examines its characters. Tree comparisons may also examine characters, potentially multiple times while navigating the tree. Which is faster depends on string lengths, prefixes, data distribution, allocator behavior, and the operation mix. Profile rather than guessing.
Cheat Sheet
- Use
std::unordered_map<Key, Value>for mostly exact lookups when average-case speed is the priority. - Use
std::map<Key, Value>when you need keys sorted. map::find(key): guaranteedO(log n).unordered_map::find(key): averageO(1), worstO(n).map::lower_bound(key): first key>= key.unordered_maphas no key-order range query.- Do not rely on
unordered_mapiteration order. - Use
findor C++20containsto test presence without insertion. operator[]inserts a default value if the key is missing.- Before bulk inserts into an unordered map, consider:
items.reserve(expectedCount);
- An unordered-map rehash invalidates iterators; map insertion does not invalidate existing iterators.
- For custom unordered keys: if
a == b, thenhash(a) == hash(b)must be true. - For custom map keys: the comparator must define a consistent strict weak ordering.
- Benchmark with representative keys and operations when the choice is performance-critical.
FAQ
Is std::unordered_map always faster than std::map?
No. It often has faster average exact lookup, but hashing, collisions, memory layout, allocations, and workload details matter. map also wins when sorted traversal or range searches avoid extra work.
Why would I use std::map if I do not currently need sorted output?
Use it if you need ordered operations such as lower_bound, threshold searches, or interval traversal. It can also be appropriate when predictable logarithmic worst-case behavior matters.
Does std::unordered_map preserve insertion order?
No. Its iteration order is unspecified and may change after a rehash. Use another container if insertion order is a requirement.
Can I get sorted output from an unordered_map?
Yes. Copy its entries into a vector and sort the vector. This is often a good design when ordered output is occasional rather than constant.
Should I call reserve on every unordered_map?
Not necessarily, but call it before a large insertion batch when you can estimate the number of elements. It can reduce rehashing and allocations.
Are std::map iterators invalidated when I insert another element?
Existing std::map iterators and references remain valid after insertion. Erasing an element invalidates iterators and references to that erased element only.
Can I use a custom class as the key in both containers?
Yes. std::map needs a valid ordering comparator. std::unordered_map needs a hash function and an equality comparison that agree with each other.
Is std::map safer against hash-collision attacks?
A map's logarithmic lookup bound does not depend on hash distribution, while an unordered map can degrade with many collisions. This can matter when keys originate from untrusted input or predictable latency is important.
Mini Project
Description
Build a small event scheduler that stores tasks by integer timestamp. It demonstrates why an ordered std::map is useful: the program must find and process every task scheduled at or after a chosen time. An unordered map can find one exact timestamp quickly, but it cannot directly start at the next timestamp in sorted order.
Goal
Store scheduled tasks and print all tasks due at or after a requested timestamp in chronological order.
Requirements
Use std::map<int, std::string> to store timestamps and task descriptions.
Keep learning
Related questions
Advantages of Brace Initialization in C++
Learn why C++ brace initialization is often clearer and safer than other object initialization styles, with examples and common pitfalls.
Basic Rules and Idioms for Operator Overloading in C++
Learn the core rules, syntax, and common idioms for operator overloading in C++, including member vs non-member operators.
C++ Aggregates, Trivial Types, Trivially Copyable Types, and PODs Explained
Learn what aggregates, trivial types, trivially copyable types, and PODs mean in C++, how they differ, and why they matter.