본문으로 건너뛰기
Previous
Next
C++ map vs unordered_map: When to Use Each and How They Work

C++ map vs unordered_map: When to Use Each and How They Work

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

Propertystd::mapstd::unordered_map
OrderingSorted by keyNo guaranteed order
LookupO(log n)Average O(1), worst O(n)
InsertO(log n)Average O(1), worst O(n)
IterationSortedUnspecified
Range querieslower_bound, upper_boundNot natural
MemoryTree nodes (extra pointers)Hash buckets + entries
Key requirementoperator< (strict weak order)std::hash<K> + operator==
Iterator invalidationInsert: stable; erase: only the erased elementRehash 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::mapunordered_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_map by default — O(1) average lookup, good for most key types
  • Use map when you need sorted iteration, range queries with lower_bound/upper_bound, or when keys aren’t hashable
  • operator[] inserts if the key is missing — use find() or at() when you don’t want that behavior
  • Erase during iteration: use it = m.erase(it) pattern, or collect keys first
  • Custom keys: map needs operator<, unordered_map needs std::hash<K> + operator==
  • reserve() on unordered_map before 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와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

C++, map, unordered_map, hash map, STL, dictionary 등으로 검색하시면 이 글이 도움이 됩니다.