C++ map vs unordered_map: When to Use Each and How They Work
이 글의 핵심
std::map (sorted tree) and std::unordered_map (hash table) both map keys to values, but with different tradeoffs. This guide covers when to use each, the operator[] pitfall, custom keys, safe iteration with erase, and practical patterns.
What Are These Containers?
Both std::map and std::unordered_map store key → value pairs and let you look up a value by its key. The difference is in how they store and find elements:
std::map: stores keys in sorted order using a balanced binary search tree (typically red-black tree). Every operation is O(log n).std::unordered_map: stores keys in hash buckets. Average O(1) lookup, but no ordering guarantee.
#include <map>
#include <unordered_map>
#include <string>
#include <iostream>
int main() {
std::map<std::string, int> ordered;
ordered["banana"] = 3;
ordered["apple"] = 1;
ordered["cherry"] = 2;
// Iterates in alphabetical order: apple, banana, cherry
for (const auto& [key, val] : ordered) {
std::cout << key << ": " << val << '\n';
}
std::unordered_map<std::string, int> hashed;
hashed["banana"] = 3;
hashed["apple"] = 1;
hashed["cherry"] = 2;
// Iteration order is unspecified — depends on hash values
for (const auto& [key, val] : hashed) {
std::cout << key << ": " << val << '\n';
}
}
Comparison Table
| Property | std::map | std::unordered_map |
|---|---|---|
| Ordering | Sorted by key | No guaranteed order |
| Lookup | O(log n) | Average O(1), worst O(n) |
| Insert | O(log n) | Average O(1), worst O(n) |
| Iteration | Sorted | Unspecified |
| Range queries | lower_bound, upper_bound | Not natural |
| Memory | Tree nodes (extra pointers) | Hash buckets + entries |
| Key requirement | operator< (strict weak order) | std::hash<K> + operator== |
| Iterator invalidation | Insert: stable; erase: only the erased element | Rehash invalidates all; erase: only erased element |
Basic Usage
#include <map>
#include <unordered_map>
#include <string>
// --- map ---
std::map<int, std::string> students;
// Insert
students[101] = "Alice";
students.insert({102, "Bob"});
students.emplace(103, "Carol"); // most efficient
// Lookup
auto it = students.find(101);
if (it != students.end()) {
std::cout << it->second << '\n'; // Alice
}
// Access — throws if missing
try {
std::string name = students.at(999); // throws std::out_of_range
} catch (const std::out_of_range& e) {
std::cerr << "Not found\n";
}
// --- unordered_map ---
std::unordered_map<std::string, int> wordCount;
// Reserve to avoid rehashing
wordCount.reserve(1000);
wordCount["hello"]++;
wordCount["world"]++;
wordCount["hello"]++;
// wordCount["hello"] == 2
The operator[] Pitfall
operator[] has a dangerous side effect: if the key doesn’t exist, it inserts a default-constructed value and returns a reference to it.
std::map<std::string, int> ages;
ages["Alice"] = 25;
// PROBLEM: this inserts "Bob" with value 0
std::cout << ages["Bob"] << '\n'; // prints 0, AND inserts "Bob"!
// ages now has {"Alice": 25, "Bob": 0}
std::cout << ages.size() << '\n'; // 2 — you created an entry accidentally
Safe alternatives:
// Option 1: find() — no insertion, returns end() if missing
auto it = ages.find("Bob");
if (it != ages.end()) {
std::cout << it->second << '\n';
} else {
std::cout << "Not found\n";
}
// Option 2: at() — no insertion, throws if missing
try {
int age = ages.at("Bob");
} catch (const std::out_of_range&) {
std::cout << "Not found\n";
}
// Option 3: contains() (C++20)
if (ages.contains("Bob")) {
std::cout << ages.at("Bob") << '\n';
}
When operator[] IS correct: when you intentionally want to insert-or-default:
// Word frequency counting — default 0 if not present, then increment
std::unordered_map<std::string, int> freq;
std::string word;
while (std::cin >> word) {
freq[word]++; // correct use of operator[] — insert 0 then increment
}
Practical Patterns
Word Frequency Counter
#include <unordered_map>
#include <string>
#include <sstream>
#include <iostream>
#include <vector>
#include <algorithm>
std::unordered_map<std::string, int> countWords(const std::string& text) {
std::unordered_map<std::string, int> freq;
std::istringstream iss(text);
std::string word;
while (iss >> word) {
freq[word]++;
}
return freq;
}
int main() {
auto freq = countWords("the quick brown fox jumps over the lazy dog the");
// Sort by frequency (descending) — copy to vector first
std::vector<std::pair<std::string, int>> sorted(freq.begin(), freq.end());
std::sort(sorted.begin(), sorted.end(),
[](const auto& a, const auto& b) { return a.second > b.second; });
for (const auto& [word, count] : sorted) {
std::cout << word << ": " << count << '\n';
}
// the: 3
// quick: 1, brown: 1, ...
}
Grouping with map (Sorted Output)
#include <map>
#include <vector>
#include <string>
struct Employee {
std::string name;
std::string department;
};
std::map<std::string, std::vector<std::string>> groupByDepartment(
const std::vector<Employee>& employees)
{
std::map<std::string, std::vector<std::string>> groups;
for (const auto& emp : employees) {
groups[emp.department].push_back(emp.name); // insert-or-append
}
return groups;
}
// Result iterates departments in alphabetical order
Range Query with map
#include <map>
#include <iostream>
int main() {
std::map<int, std::string> events = {
{2020, "Event A"}, {2021, "Event B"}, {2022, "Event C"},
{2023, "Event D"}, {2024, "Event E"}
};
// Find all events in [2021, 2023]
auto begin = events.lower_bound(2021);
auto end = events.upper_bound(2023); // one past 2023
for (auto it = begin; it != end; ++it) {
std::cout << it->first << ": " << it->second << '\n';
}
// 2021: Event B
// 2022: Event C
// 2023: Event D
}
This range query pattern is only possible with std::map — unordered_map has no ordering.
Erasing During Iteration
The safe pattern uses the return value of erase():
std::map<std::string, int> scores = {
{"Alice", 85}, {"Bob", 42}, {"Carol", 91}, {"Dave", 38}
};
// Remove entries with score below 50
auto it = scores.begin();
while (it != scores.end()) {
if (it->second < 50) {
it = scores.erase(it); // erase returns iterator to next element
} else {
++it;
}
}
// scores: {"Alice": 85, "Carol": 91}
// Alternative: collect keys first, then erase (clearer intent)
std::vector<std::string> toRemove;
for (const auto& [name, score] : scores) {
if (score < 50) toRemove.push_back(name);
}
for (const auto& name : toRemove) {
scores.erase(name);
}
Custom Key Types
Custom Key for std::map
Need operator< (strict weak ordering):
struct Point {
int x, y;
// Required for map<Point, ...>
bool operator<(const Point& other) const {
if (x != other.x) return x < other.x;
return y < other.y;
}
};
std::map<Point, std::string> locations;
locations[{0, 0}] = "origin";
locations[{1, 2}] = "near";
Custom Key for std::unordered_map
Need std::hash specialization and operator==:
struct Point {
int x, y;
bool operator==(const Point& other) const {
return x == other.x && y == other.y;
}
};
// Hash specialization in std namespace
template<>
struct std::hash<Point> {
std::size_t operator()(const Point& p) const {
std::size_t h1 = std::hash<int>{}(p.x);
std::size_t h2 = std::hash<int>{}(p.y);
return h1 ^ (h2 << 32); // simple combination
}
};
std::unordered_map<Point, std::string> grid;
grid[{0, 0}] = "origin";
Performance Notes
unordered_map is faster for lookups but has higher worst-case behavior when there are hash collisions:
Average Worst Case
unordered_map O(1) O(n) — all keys hash to same bucket
map O(log n) O(log n) — always
Memory:
unordered_map Higher overhead (load factor, buckets)
map Lower for small maps, higher for large (tree nodes)
For large maps: unordered_map with reserve() is typically 2-5x faster for lookups.
std::unordered_map<int, std::string> m;
m.reserve(10000); // pre-allocate for ~10000 elements
m.max_load_factor(0.7f); // rehash when 70% full (default is 1.0)
Key Takeaways
- Use
unordered_mapby default — O(1) average lookup, good for most key types - Use
mapwhen you need sorted iteration, range queries withlower_bound/upper_bound, or when keys aren’t hashable operator[]inserts if the key is missing — usefind()orat()when you don’t want that behavior- Erase during iteration: use
it = m.erase(it)pattern, or collect keys first - Custom keys:
mapneedsoperator<,unordered_mapneedsstd::hash<K>+operator== reserve()onunordered_mapbefore large batch inserts to avoid repeated rehashing
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Master C++ std::map and std::unordered_map: ordered vs hash-based lookup, iterator invalidation, operator[] pitfalls, cu… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- [Hash Table | O(1) Search Data Structure Complete Guide](/en/blog/algorithm-series-03-hash-table/
- C++ set/unordered_set | ‘중복 제거’ 완벽 가이드
- C++ string | ‘문자열 처리’ 완벽 가이드 [실전 함수 총정리]
- C++ STL vector | ‘배열보다 편한’ 벡터 완벽 정리 [실전 예제]
이 글에서 다루는 키워드 (관련 검색어)
C++, map, unordered_map, hash map, STL, dictionary 등으로 검색하시면 이 글이 도움이 됩니다.