std::map / std::unordered_map
std::map (sorted red-black tree, O(log n)) and std::unordered_map (hash table, O(1) avg): when to pick each, how to tune performance, and C++17/20/23 additions.
std::map / std::unordered_mapsince C++98 / C++11std::map<K,V> is an ordered associative container backed by a red-black tree that maps unique keys to values with O(log n) operations and guaranteed sorted traversal; std::unordered_map<K,V> (C++11) provides the same key-value mapping via a hash table with O(1) average access at the cost of key ordering.
Overview
Three containers cover the key-value use case in modern C++:
| Container | Backing structure | Lookup | Iteration | Header |
|---|---|---|---|---|
std::map | Red-black tree | O(log n) | Sorted ascending | <map> |
std::unordered_map (C++11) | Hash table + chaining | O(1) avg, O(n) worst | Unordered | <unordered_map> |
std::flat_map (C++23) | Two sorted vectors | O(log n) | Sorted, cache-friendly | <flat_map> |
Choose std::map when sorted iteration, range queries, or stable iterators across insertions matter. Every insertion leaves existing iterators and references valid β a guarantee std::unordered_map cannot make across a rehash.
Choose std::unordered_map when lookup throughput dominates and key order is irrelevant. On large maps, expect 3β5Γ faster average lookup than std::map, but account for rehashing overhead and cache-hostile bucket traversal under high collision rates.
Choose std::flat_map (C++23) for read-heavy workloads built once and queried many times. Keys and values sit in contiguous vectors, eliminating per-node heap allocations and yielding substantially better cache performance than either node-based container. Insert and erase are O(n), so it is not appropriate for dynamic workloads.
Both std::map and std::unordered_map enforce unique keys. Use std::multimap / std::unordered_multimap for duplicate keys.
Syntax
// std::map β Key must satisfy LessThanComparable (operator< or custom Compare)
template<
class Key,
class T,
class Compare = std::less<Key>, // C++98
class Allocator = std::allocator<std::pair<const Key, T>>
> class map;
// std::unordered_map β Key must satisfy Hash and EqualityComparable
template<
class Key,
class T,
class Hash = std::hash<Key>, // C++11
class KeyEqual = std::equal_to<Key>,
class Allocator = std::allocator<std::pair<const Key, T>>
> class unordered_map;value_type for both is std::pair<const Key, T>. The const on Key is load-bearing: it prevents in-place key mutation, which would silently corrupt the container's invariant. Use extract() (C++17) when you need to modify a key.
Insertion Methods
Four distinct insertion methods exist, and choosing the wrong one costs either correctness or performance. They differ in two dimensions: whether they overwrite an existing value, and when they construct the mapped value.
#include <map>
#include <string>
#include <vector>
std::map<std::string, std::vector<int>> m;
// --- insert(value) --------------------------------------------------------
// C++98. Inserts only if key is absent. Returns {iterator, bool}.
// The pair is constructed before the call, even if the key already exists β
// a wasted construction if key is present.
auto [it1, ok1] = m.insert({"alpha", {1, 2, 3}}); // C++17 structured bindings
// ok1 == true β inserted; ok1 == false β key existed, nothing changed.
// --- emplace(args...) ------------------------------------------------------
// C++11. Constructs in-place from forwarded arguments. Avoids one copy vs.
// insert(). Still constructs the value before checking key existence.
auto [it2, ok2] = m.emplace("beta", std::vector<int>{4, 5});
// --- try_emplace(key, args...) --------------------------------------------
// C++17. Constructs the VALUE in-place ONLY if the key is absent. If the
// key already exists, the arguments are left untouched β no move, no copy,
// no construction+destruction cycle. Strictly preferable over operator[] or
// emplace for conditional insertion with expensive or move-only values.
auto [it3, ok3] = m.try_emplace("gamma", std::initializer_list<int>{6, 7});
m.try_emplace("gamma", std::initializer_list<int>{99}); // key exists β {6,7} kept, {99} never built
// --- insert_or_assign(key, val) -------------------------------------------
// C++17. Always assigns the value, whether the key is new or not.
// Returns {iterator, bool}: bool is true if inserted, false if assigned.
// Semantically equivalent to operator[] + assignment, but works on rvalues
// and does not require DefaultConstructible.
auto [it4, ok4] = m.insert_or_assign("alpha", std::vector<int>{10, 11});
// ok4 == false β "alpha" existed and was overwritten
// --- operator[] -----------------------------------------------------------
// C++98. Default-constructs the value if key is absent, THEN returns a ref.
// Never use for existence checks β it mutates the map unconditionally.
m["delta"].push_back(42); // ok: intentional default-init + modify patternBest Practice. Default to try_emplace for any "insert if not present" operation. Use insert_or_assign when you always want the new value to win. Reserve operator[] for the explicit "upsert + modify" idiom (freq[word]++) where the default value of zero is meaningful.
The decision tree in one sentence: need to overwrite? β insert_or_assign. Inserting only? β try_emplace. Building the value unconditionally is fine? β emplace or insert.
Lookup Methods
Every lookup method in the standard library is const-qualified and safe to call without mutating the container.
#include <map>
#include <string>
std::map<std::string, int> scores{
{"Alice", 95}, {"Bob", 87}, {"Carol", 72}
};
// --- find(key) --- C++98 ---------------------------------------------------
// Returns an iterator to the element, or end() if not found.
// Prefer this when you need the iterator (to modify the value, erase, etc.).
if (auto it = scores.find("Alice"); it != scores.end()) { // C++17 init-if
std::cout << it->second << '\n'; // 95
}
// --- count(key) --- C++98 --------------------------------------------------
// Returns 0 or 1 for std::map (always unique keys).
// Exists primarily for multimap compatibility. Avoid in new code.
bool exists = scores.count("Bob") > 0; // C++98 idiom
// --- contains(key) --- C++20 -----------------------------------------------
// Returns bool directly. Clearer intent than count() > 0. No insertion.
if (scores.contains("Carol")) { ... } // C++20 β preferred existence check
// --- lower_bound / upper_bound --- C++98 -----------------------------------
// lower_bound(k): first element with key >= k
// upper_bound(k): first element with key > k
// Both return end() if no such element exists.
auto lo = scores.lower_bound("Bob"); // β "Bob" entry
auto hi = scores.upper_bound("Bob"); // β "Carol" entry (first after "Bob")
// Iterate the closed interval ["Bob", "Carol"]:
for (auto it = scores.lower_bound("Bob"); it != scores.upper_bound("Carol"); ++it) {
std::cout << it->first << ": " << it->second << '\n';
}
// --- equal_range(key) --- C++98 --------------------------------------------
// Returns std::pair{lower_bound(key), upper_bound(key)} in one call.
// For std::map the range contains at most one element. Most useful for multimap.
auto [beg, end] = scores.equal_range("Alice"); // C++17 structured bindings
for (auto it = beg; it != end; ++it) {
std::cout << it->second << '\n'; // 95
}Best Practice. Use contains() (C++20) for pure existence checks. Use find() when you need the iterator to act on the element afterwards β never call find() and then operator[] on the same key; that is two O(log n) lookups where one suffices.
lower_bound and upper_bound are exclusive to ordered containers (std::map, not std::unordered_map). For sorted iteration over a key range, they are far more efficient than scanning all elements.
Node Handle Operations (C++17)
Node handles allow nodes to be removed from a container and reinserted elsewhere β without any allocation, deallocation, or copy of the stored key or value. The extract() member function returns a node_type handle that owns the node's memory until it is either reinserted or destroyed.
This mechanism solves a long-standing pain point: changing a map key without erasing and re-inserting (the only pre-C++17 alternative).
#include <map>
#include <string>
std::map<int, std::string> m{{1,"one"},{2,"two"},{3,"three"}};
// --- Rename a key (the classic node-handle use case) ----------------------
auto node = m.extract(2); // removes node from m; no allocation or copy
// node.key() returns a non-const reference β mutation is legal here
node.key() = 42;
m.insert(std::move(node)); // spliced back in; no allocation or copy
// m is now: {1:"one", 3:"three", 42:"two"}
// --- Transfer nodes between maps ------------------------------------------
std::map<int, std::string> src{{10,"a"},{20,"b"},{30,"c"}};
std::map<int, std::string> dst{{20,"X"}};
// insert() with a node_type returns an insert_return_type {position, inserted, node}
auto result = dst.insert(src.extract(10)); // 10 not in dst β inserted
// result.inserted == true; result.node is empty
auto result2 = dst.insert(src.extract(20)); // 20 already in dst β not inserted
// result2.inserted == false; result2.node holds the extracted {20,"b"} β not lost!
// --- merge() ---------------------------------------------------------------
// C++17. Moves all nodes from src whose keys are not in dst. Conflicting nodes
// stay in src. No allocations or value copies occur.
std::map<int,std::string> a{{1,"a"},{2,"b"}};
std::map<int,std::string> b{{2,"X"},{3,"c"}};
a.merge(b);
// a β {1:"a", 2:"b", 3:"c"} (3 transferred)
// b β {2:"X"} (2 stayed β conflict)Best Practice. Use extract() + key mutation as the canonical way to rename a map key. The pre-C++17 pattern of erasing and reinserting copies the value; extract() avoids that entirely.
A node_type that falls out of scope without being reinserted frees the node automatically β no leak occurs. However, an uninserted node holding a key conflict (as in result2 above) represents a value that would otherwise be silently discarded; check inserted before assuming success.
Custom Comparator and Heterogeneous Lookup
std::map with std::less<> (C++14)
The default comparator std::less<Key> requires the lookup argument to be the exact key type. Substituting std::less<> (the void specialisation) enables heterogeneous lookup: find, contains, count, lower_bound, upper_bound, and equal_range all accept any type comparable to Key via operator<, without constructing a temporary Key.
#include <map>
#include <string>
#include <string_view>
// Without heterogeneous lookup β constructs a temporary std::string each call
std::map<std::string, int> m1{{"hello", 1}};
m1.find("hello"); // constructs std::string("hello") β C++98
// With heterogeneous lookup β zero allocation
std::map<std::string, int, std::less<>> m2{{"hello", 1}}; // C++14
std::string_view sv = "hello";
m2.find(sv); // no std::string constructed β C++14
m2.contains(sv); // C++20 + C++14 transparent comparator
m2.lower_bound("he"); // C++14 transparent β string_view or const char* acceptedCustom comparator struct
When the default less-than ordering is not appropriate β for example, case-insensitive string keys β supply a custom comparator:
#include <map>
#include <string>
#include <cctype>
#include <algorithm>
struct CaseInsensitiveLess {
// is_transparent opts-in to heterogeneous lookup in std::map
using is_transparent = void;
bool operator()(std::string_view a, std::string_view b) const noexcept {
return std::lexicographical_compare(
a.begin(), a.end(),
b.begin(), b.end(),
[](unsigned char x, unsigned char y) {
return std::tolower(x) < std::tolower(y);
}
);
}
};
std::map<std::string, int, CaseInsensitiveLess> m;
m["Hello"] = 1;
m.find("HELLO"); // finds "Hello" β no new std::string constructed
m.find("hello"); // same elementThe using is_transparent = void marker is how the standard library detects that a comparator supports heterogeneous lookup. Without it, std::map refuses to compile heterogeneous find calls.
Heterogeneous lookup in std::unordered_map (C++20)
std::unordered_map requires both the hash and the equality predicate to declare is_transparent before heterogeneous lookup is enabled:
#include <unordered_map>
#include <string>
#include <string_view>
struct SvHash {
using is_transparent = void;
size_t operator()(std::string_view sv) const noexcept {
return std::hash<std::string_view>{}(sv);
}
size_t operator()(const std::string& s) const noexcept {
return std::hash<std::string_view>{}(s);
}
};
struct SvEqual {
using is_transparent = void;
bool operator()(std::string_view a, std::string_view b) const noexcept {
return a == b;
}
};
// C++20: both Hash and KeyEqual must have is_transparent
std::unordered_map<std::string, int, SvHash, SvEqual> um;
um["world"] = 42;
std::string_view sv = "world";
um.find(sv); // zero allocation β C++20
um.contains(sv); // zero allocation β C++20Common Pitfall. Providing is_transparent only on the hash functor but not on KeyEqual (or vice versa) silently disables heterogeneous lookup β the call compiles but constructs a temporary key anyway.
std::unordered_map Hash Customization
std::hash is specialised for built-in types, std::string, std::string_view, and a handful of others. For user-defined types you must provide a hash functor.
Hash combining for composite keys
XOR (^) is a poor hash combiner because it is symmetric: hash(a, b) == hash(b, a). This collapses all permutations of a key into the same bucket. The canonical combining pattern uses a multiplicative offset based on the golden ratio:
#include <unordered_map>
#include <functional>
struct Point { int x, y; };
// Specialise std::hash in namespace std for seamless integration
namespace std {
template<>
struct hash<Point> {
size_t operator()(const Point& p) const noexcept {
// Boost-style hash_combine using the golden ratio constant 0x9e3779b9
size_t h = std::hash<int>{}(p.x);
h ^= std::hash<int>{}(p.y) + 0x9e3779b9 + (h << 6) + (h >> 2);
return h;
}
};
}
std::unordered_map<Point, double> distances;
distances[{0, 0}] = 0.0;
distances[{3, 4}] = 5.0;Alternatively, supply the hash as the third template argument to avoid touching namespace std:
struct PointHash {
size_t operator()(const Point& p) const noexcept {
size_t h = std::hash<int>{}(p.x);
h ^= std::hash<int>{}(p.y) + 0x9e3779b9 + (h << 6) + (h >> 2);
return h;
}
};
struct PointEqual {
bool operator()(const Point& a, const Point& b) const noexcept {
return a.x == b.x && a.y == b.y;
}
};
std::unordered_map<Point, double, PointHash, PointEqual> dist2;Reserving capacity
std::unordered_map<std::string, int> freq;
freq.reserve(50'000); // pre-allocates buckets; prevents rehashing during bulk insert
freq.max_load_factor(0.5f); // rehash when load > 0.5; lower = fewer collisions, more memory
// default max_load_factor is 1.0Best Practice. Always call reserve(n) before inserting a known number of elements into an unordered_map. A default-constructed map with no reservation causes O(log n) rehashes during a bulk insert of n elements, each one invalidating all existing iterators.
std::flat_map (C++23)
std::flat_map stores keys and values in two separate sorted vectors rather than a tree of heap-allocated nodes. This layout enables:
- Contiguous memory iteration (hardware prefetcher-friendly)
- No per-element allocation overhead
- Binary search over a packed key array (good branch prediction)
The trade-off is that insert and erase are O(n) because elements must be shifted to maintain sorted order.
#include <flat_map> // C++23
// Construction from existing sorted data β O(n), no sorting needed
// Mark with std::sorted_unique to skip the sort step
std::vector<int> keys = {1, 3, 5, 7, 10};
std::vector<std::string> values = {"one","three","five","seven","ten"};
std::flat_map<int, std::string> fm(
std::sorted_unique, // C++23: assert data is already sorted+unique
std::move(keys),
std::move(values)
);
// Lookup is identical to std::map
auto it = fm.find(5); // O(log n) binary search over contiguous keys
fm.contains(7); // C++20 + C++23
// Batch insertion is efficient; random one-at-a-time insertion is not
fm.insert(fm.end(), {12, "twelve"}); // hint at end() for amortized O(1) when appending
// Underlying containers are accessible
auto& ks = fm.keys(); // const ref to the sorted key vector
auto& vs = fm.values(); // const ref to the value vectorImportant caveat: no stable iterators. Any insertion or erasure invalidates all iterators and references to elements in a flat_map. This is fundamentally different from std::map, where only the erased element's iterator is invalidated. Do not cache flat_map iterators across mutations.
// WRONG β iterator i is invalidated by insert
auto i = fm.find(3);
fm[15] = "fifteen"; // insertion shifts everything; i is dangling
std::cout << i->second; // undefined behaviour
// CORRECT β re-look up after mutation
fm[15] = "fifteen";
auto j = fm.find(3); // safeflat_map is ideal for configuration tables, lookup tables, and other "build once, query many times" patterns where the set of keys is fixed after initialisation.
Iterator Invalidation
Iterator invalidation rules differ substantially between the three containers and are a common source of bugs.
| Container | On insert | On erase | On rehash |
|---|---|---|---|
std::map | No invalidation | Only the erased element | N/A (no rehash) |
std::unordered_map | No invalidation (unless rehash triggered) | Only the erased element | All iterators invalidated |
std::flat_map | All iterators invalidated | All iterators invalidated | N/A |
For std::unordered_map, a rehash is triggered when size() > max_load_factor() * bucket_count() after an insertion. Calling reserve(n) beforehand prevents rehashing during a known bulk insertion.
// Safe erase-while-iterating for std::map and std::unordered_map:
// erase() returns the next valid iterator in C++11 and later.
for (auto it = m.begin(); it != m.end(); ) {
if (shouldErase(it->second)) {
it = m.erase(it); // C++11: erase returns next iterator
} else {
++it;
}
}
// For std::flat_map, prefer the erase-remove idiom or rebuild from scratch
// if erasing many elements, since each individual erase is O(n).std::map provides the strongest guarantee: insertions never invalidate any iterator or reference, and erasure invalidates only the erased element's iterator. This makes it safe to hold long-lived iterators as stable handles into the map β a pattern not available with unordered_map or flat_map.
Structured Bindings with Maps (C++17)
std::map::value_type is std::pair<const Key, T>. Structured bindings unpack this cleanly in range-for loops.
#include <map>
#include <string>
std::map<std::string, int> scores{{"Alice", 95}, {"Bob", 87}, {"Carol", 72}};
// Read-only iteration β preferred; avoids accidental modification
for (const auto& [name, score] : scores) { // C++17
std::cout << name << ": " << score << '\n';
}
// Mutating values β auto& binds to the pair; v is a ref to pair.second
for (auto& [name, score] : scores) {
score += 5; // modifies in-place β correct
}
// Common mistake: auto (by value) silently copies, mutation is a no-op
for (auto [name, score] : scores) { // copies each pair
score = 0; // modifies the copy only β silent no-op on the map
}
// Structured bindings also work with insert/emplace return values
auto [it, inserted] = scores.try_emplace("Dave", 88); // C++17
if (inserted) {
std::cout << "Added " << it->first << '\n';
}
// And with equal_range
auto [lo, hi] = scores.equal_range("Bob");
for (auto it = lo; it != hi; ++it) {
std::cout << it->second << '\n';
}Common Pitfall. auto [k, v] in a range-for copies the entire pair<const Key, T>. For maps with non-trivial value types this is both a performance and a correctness hazard. Always use const auto& [k, v] for read-only access and auto& [k, v] when mutation is intended.
The key binding k always has the type const Key& even with auto& [k, v] β the const on the key comes from the value_type, not from the range-for binding, reinforcing the invariant that keys cannot be mutated through normal iteration.
Best Practices
- Use
try_emplaceoveroperator[]for any value type that is expensive to construct, move, or should not be default-initialized.operator[]always default-constructs the value on a miss. - Call
reserve(n)before bulk insertion intounordered_map. A default-constructed map rehashes roughly every time the element count crosses a power-of-two threshold β inserting 100k elements without reserving causes ~17 rehashes. - Enable heterogeneous lookup (
std::less<>for map,is_transparenthash/equal for unordered_map) whenever the key type is heavier than the lookup key (e.g.,std::stringkeyed map queried withstd::string_view). - Prefer
contains()overcount()in C++20 and later.count()returns 0 or 1 for unique-key maps and exists mainly for multimap compatibility;contains()expresses intent clearly. - After
find(), act on the iterator rather than callingoperator[]again. A find-then-index pattern does two lookups for one key. - Use
extract()to rename a key rather than erase + reinsert.extract()transfers the node in O(log n) without touching the mapped value. - Use
flat_mapfor read-dominated, fixed-size tables. If the set of keys is built once at startup and queried millions of times,flat_maptypically outperforms bothmapandunordered_mapdue to cache locality. - Never hold
unordered_mapiterators across insertions unless you have pre-reserved sufficient capacity. A single insert that crosses the load factor threshold invalidates every iterator and reference in the container.
Common Pitfalls
operator[] is always an insertion
std::map<std::string, int> m;
if (m["missing"]) { ... } // "missing" β 0 is now in the map β silent mutation
// Fix: m.contains("missing") in C++20, or m.find("missing") != m.end()This is particularly dangerous in const member functions β operator[] is non-const and will not compile there, but find() and contains() are both const-qualified.
Hash flooding degrades unordered_map to O(n)
If many keys collide into the same bucket β through a poor hash function or adversarial input β lookup degrades from O(1) to O(n). For code that processes externally-supplied keys, use a randomised seed (e.g., FNV-1a with a per-process secret) or fall back to std::map.
flat_map iterators are unconditionally invalidated by mutation
std::flat_map<int, int> fm{{1,10},{2,20},{3,30}};
auto it = fm.find(2);
fm[4] = 40; // insertion invalidates it
it->second; // undefined behaviour β it is danglingThere is no "safe insertion that does not invalidate" for flat_map, unlike std::map.
Iterator invalidation rules differ
std::map: insertions never invalidate any iterator or reference. Only the erased element's iterator is invalidated on erase.
std::unordered_map: any insertion that triggers a rehash invalidates all iterators. After a rehash, only the erased element's iterator is invalidated on erase.
// Safe erase-during-iteration for both containers:
for (auto it = m.begin(); it != m.end(); ) {
it = shouldErase(it->second) ? m.erase(it) : std::next(it);
}Structured bindings and const correctness
for (auto& [k, v] : m) { v = 0; } // modifies values in-place β correct
for (auto [k, v] : m) { v = 0; } // modifies a copy β silent no-op
for (const auto& [k, v] : m) { ... } // read-only β preferredAlways use const auto& when iterating for read-only access; auto& when mutating values.
is_transparent must be set on both hash and equality for unordered_map
// WRONG β only hash is transparent; heterogeneous lookup silently disabled
struct HalfTransparentHash {
using is_transparent = void;
size_t operator()(std::string_view sv) const { return std::hash<std::string_view>{}(sv); }
};
// KeyEqual = std::equal_to<std::string> β no is_transparent β heterogeneous lookup disabled
std::unordered_map<std::string, int, HalfTransparentHash> broken;
std::string_view sv = "key";
broken.find(sv); // still constructs a temporary std::string β no error, no warningBoth the hash type and the equality type must declare using is_transparent = void for C++20 heterogeneous lookup to activate in unordered_map.
See Also
std::set/std::unordered_setβ same containers without a mapped valuestd::multimap/std::unordered_multimapβ allow duplicate keysstd::flat_map(C++23) β cache-efficient sorted map over contiguous storage- Which container should I use?
- Structured bindings β unpacking
pair<const K,V>in range-for