struct Expensive { std::string data; Expensive(std::string d) : data(std::move(d)) { std::cout << ” Constructed Expensive: ” << data << “\n”; } };
int main() { std::map<std::string, Expensive> m;
// try_emplace only constructs if key is absent [N4950 §22.4.4.4]
std::cout << "First insert:\n";
m.try_emplace("key1", "value1"); // Constructs Expensive
std::cout << "Second insert (same key):\n";
m.try_emplace("key1", "should_not_construct"); // No construction!
std::cout << "Third insert (new key):\n";
m.try_emplace("key2", "value2"); // Constructs Expensive
for (const auto& [k, v] : m) {
std::cout << k << " -> " << v.data << "\n";
}
### `std::unordered_map` and `std::unordered_set`: Hash Table, O(1) Average
`std::unordered_map` and `std::unordered_set` are **unordered associative containers** that use a
Hash table for $O(1)$ average-case insertion, deletion, and lookup [N4950 §22.5]. The standard
Specifies that each bucket holds a singly-linked list of elements that hash to the same value, and
Rehashing occurs when the load factor exceeds `max_load_factor()` [N4950 §22.5.5].
#### Hash Table Collision Resolution
The standard mandates **separate chaining** for collision resolution [N4950 §22.5.1]: each bucket
Contains a singly-linked list of all elements whose key hashes to that bucket index. When two keys
Produce the same bucket index (a **collision**), they are stored in the same linked list.
\mathrm{bucket\_index = H(\mathrm{key) \mod \mathrm{bucket\_count
The average chain length equals the load factor:
\mathrm{avg\_chain\_length = \frac{\mathrm{size()}{\mathrm{bucket\_count()} = \mathrm{load\_factor
With default `max_load_factor = 1.0`The average chain length is kept below 1.0, meaning most Lookups
require at most one equality comparison. When the load factor exceeds `max_load_factor`The Container
**rehashes**: allocates a new bucket array ( $2\times$ the old count), recomputes Bucket indices for
all elements, and deallocates the old array. This is $O(n)$ but occurs Infrequently.
The **load factor** is defined as [N4950 §22.5.5.3]:
\mathrm{load\_factor = \frac{\mathrm{size()}{\mathrm{bucket\_count()}
The default `max_load_factor()` is `1.0` [N4950 §22.5.5.3].
// std::unordered_set: O(1) average lookup [N4950 §22.5.6]
std::unordered_set<int> us = {5, 3, 1, 4, 2};
std::cout << "bucket_count=" << us.bucket_count() << "\n";
std::cout << "load_factor=" << us.load_factor() << "\n";
std::cout << "max_load_factor=" << us.max_load_factor() << "\n";
std::cout << "contains(3): " << us.contains(3) << "\n"; // 1
// std::unordered_map: O(1) average lookup [N4950 §22.5.4]
std::unordered_map<std::string, int> word_count;
// Bucket interface [N4950 §22.5.5]
for (std::size_t i = 0; i < word_count.bucket_count(); ++i) {
std::cout << "bucket " << i << " has " << word_count.bucket_size(i) << " elements\n";
// Rehash to a specific bucket count [N4950 §22.5.5.3]
std::cout << "After rehash(16): bucket_count=" << word_count.bucket_count() << "\n";
The hash function H used by std::unordered_map and std::unordered_set must satisfy [N4950 §22.5.3.2]:
H is a function object whose return type is std::size_tIf h1(k1) == h2(k2) and k1 == k2Then h1(k1) == h2(k2) must hold for the same key If k1 == k2Then H(k1) == H(k2) must hold H must not throw exceptionsThe default hash std::hash<T> is specialized for all arithmetic types, std::string std::string_viewAnd all standard smart pointer types [N4950 §22.14.3]. For custom types, you Must provide a specialization:
bool operator == ( const Point & other ) const {
return x == other.x && y == other.y;
// Custom hash for Point [N4950 §22.14.3]
std :: size_t operator () ( const Point & p ) const noexcept {
// Combine two hash values using a standard technique
std :: size_t hx = std :: hash <double> {}(p.x);
std :: size_t hy = std :: hash <double> {}(p.y);
return hx ^ (hy << 1 ); // Simple combining — see boost::hash_combine for better
std :: unordered_map < Point, std :: string, PointHash > locations;
locations[{ 1.0 , 2.0 }] = " origin " ;
locations[{ 3.5 , 4.5 }] = " somewhere " ;
auto it = locations. find (query);
if (it != locations. end ()) {
std :: cout << " Found: " << it -> second << " \n " ; // "origin"
std::multimap (and std::multiset) allow duplicate keys. Elements with equivalent keys are stored In insertion order. The equal_range member function returns a pair of iterators defining the range Of elements with a given key [N4950 §22.4.4.4]:
std :: multimap < std :: string, int> scores;
scores. insert ({ " Alice " , 90 });
scores. insert ({ " Bob " , 85 });
scores. insert ({ " Alice " , 95 });
scores. insert ({ " Alice " , 88 });
// equal_range returns [first, last) of all elements with key "Alice"
auto [first, last] = scores. equal_range ( " Alice " );
std :: cout << " Alice's scores: " ;
for ( auto it = first; it != last; ++ it) {
std :: cout << it -> second << " " ;
// Output: Alice's scores: 90 95 88 (insertion order preserved)
std :: cout << " \n Count of 'Alice': " << scores. count ( " Alice " ) << " \n " ;
// Output: Count of 'Alice': 3
constexpr std :: size_t N = 1'000'000 ;
std :: vector < std :: string > keys;
for ( std :: size_t i = 0 ; i < N; ++ i) {
keys. push_back ( " key_ " + std :: to_string (i));
// Benchmark std::map (red-black tree, O(log n))
std :: map < std :: string, int> m;
auto start = std :: chrono :: high_resolution_clock :: now ();
for ( std :: size_t i = 0 ; i < N; ++ i) {
m[keys[i]] = static_cast<int> (i);
auto end = std :: chrono :: high_resolution_clock :: now ();
auto ms = std :: chrono :: duration_cast < std :: chrono :: milliseconds >(end - start). count ();
std :: cout << " std::map insert " << N << " : " << ms << " ms \n " ;
start = std :: chrono :: high_resolution_clock :: now ();
for ( std :: size_t i = 0 ; i < N; ++ i) {
std :: size_t idx = rng () % N;
volatile auto val = m. find (keys[idx]);
end = std :: chrono :: high_resolution_clock :: now ();
ms = std :: chrono :: duration_cast < std :: chrono :: milliseconds >(end - start). count ();
std :: cout << " std::map lookup " << N << " : " << ms << " ms \n " ;
// Benchmark std::unordered_map (hash table, O(1) avg)
std :: unordered_map < std :: string, int> m;
auto start = std :: chrono :: high_resolution_clock :: now ();
for ( std :: size_t i = 0 ; i < N; ++ i) {
m[keys[i]] = static_cast<int> (i);
auto end = std :: chrono :: high_resolution_clock :: now ();
auto ms = std :: chrono :: duration_cast < std :: chrono :: milliseconds >(end - start). count ();
std :: cout << " std::unordered_map insert " << N << " : " << ms << " ms \n " ;
start = std :: chrono :: high_resolution_clock :: now ();
for ( std :: size_t i = 0 ; i < N; ++ i) {
std :: size_t idx = rng () % N;
volatile auto val = m. find (keys[idx]);
end = std :: chrono :: high_resolution_clock :: now ();
ms = std :: chrono :: duration_cast < std :: chrono :: milliseconds >(end - start). count ();
std :: cout << " std::unordered_map lookup " << N << " : " << ms << " ms \n " ;
Criterion map / setunordered_map / unordered_setOrdering Sorted by key No ordering Lookup O ( log n ) O(\log n) O ( log n ) worst caseO ( 1 ) O(1) O ( 1 ) average, O ( n ) O(n) O ( n ) worst caseInsert O ( log n ) O(\log n) O ( log n ) O ( 1 ) O(1) O ( 1 ) averageMemory ~3 pointers per node bucket array + 1 pointer per node Key requirement operator<operator== and hashRange queries Supported (lower_bound, upper_bound) Not supported Iterator invalidation Only on erase On rehash
Hash to the same bucket). If adversarial inputs are a concern, consider hash functions resistant to Collision attacks, or use `std::map` for guaranteed $O(\log n)$ worst-case. ### `std::map` Heterogeneous Lookup (C++14)By default, std::map::find``std::map::countAnd std::map::lower_bound accept const Key& and Require an exact key type match. If you want to look up a std::string key using a std::string_view without constructing a temporary std::stringYou need a transparent Comparator [N4950 §22.4.4.1]. The standard library provides std::less<void> (C++14), which Enables heterogeneous lookup when used as the comparator:
// Transparent comparator enables heterogeneous lookup [N4950 §22.4.4.1]
std :: map < std :: string, int , std :: less <>> m;
// Lookup with string_view — no temporary std::string constructed
std :: string_view sv = " hello " ;
std :: cout << " Found: " << it -> first << " -> " << it -> second << " \n " ;
// count, lower_bound, upper_bound, equal_range also support heterogeneous lookup
std :: cout << " count( \" world \" ): " << m. count ( " world " ) << " \n " ;
The mechanism works because std::less<void> deduces the argument types and invokes operator< as a < b where a and b may be different types, as long as the comparison is Well-formed [N4950 §22.14.3]. Without std::less<void>Calling m.find(sv) would construct a Temporary std::string from the std::string_viewIncurring a heap allocation. With heterogeneous Lookup, the comparison sv < "hello" and "hello" < sv are both valid via std::string::operator< accepting std::string_view [N4950 §23.4.5.2.2].
The same transparent comparator technique applies to std::set``std::multimapAnd std::multiset.
C++17 introduced node handles and the extract() / insert() API for associative containers [N4950 §22.4.4.4]. A node handle (std::map::node_type) owns an extracted element including its key And value. This enables moving elements between containers without copying or reallocating :
std :: map < std :: string, int> src;
std :: map < std :: string, int> dst;
// Extract a node from src — no copy, no deallocation
auto nh = src. extract ( " bravo " );
std :: cout << " Extracted: " << nh. key () << " -> " << nh. mapped () << " \n " ;
// Modify the key before insertion (unique to extract/insert)
// Insert into dst — transfers ownership, no allocation
dst. insert ( std :: move (nh));
for ( const auto& [k, v] : src) std :: cout << k << " " ;
for ( const auto& [k, v] : dst) std :: cout << k << " ( " << v << " ) " ;
Output:
This API is critical for performance-sensitive code that needs to transfer elements between maps (such as sharding or repartitioning), because the alternative — erase from one map, then emplace Into another — involves a redundant deallocation and allocation. With extract/insertThe node’s Heap memory is reparented [N4950 §22.4.4.4].
Each node in a std::map or std::set stores three pointers (parent, left child, right child) plus A color bit, in addition to the key (and value for map). On 64-bit systems, the per-node overhead Is at least 32 bytes:
+--------+--------+--------+----------+
| left* | right* | parent*| key |
| 8 bytes| 8 bytes| 8 bytes| sizeof(K)|
+--------+--------+--------+----------+
(plus color bit, alignment padding, and sizeof(V) for map)
For std::map<std::string, int>Each node costs approximately 48-64 bytes: 24 bytes of tree Pointers + sizeof(std::string) (32 bytes) + sizeof(int) (4 bytes) + alignment padding. The Default-constructed empty std::map has zero heap allocation; the first insert allocates the root Node.
Unlike sequence containers, associative containers have simpler invalidation rules [N4950 §22.4.4.1 Table 83]:
Operation Iterator effect Pointer/Reference effect insertNo invalidation No invalidation emplaceNo invalidation No invalidation eraseOnly iterators to the erased element Only to the erased element clearAll invalidated All invalidated swapNo invalidation (elements exchanged) No invalidation (elements exchanged)
This is a consequence of the node-based structure: inserting a new node allocates a new heap block That does not affect existing nodes. Erasing a node only frees that specific block.
For std::unordered_mapThe rules are stricter: rehashing invalidates all iterators, pointers, And references because elements are moved to new bucket locations [N4950 §22.5.5 Table 89].
C++20 extended heterogeneous lookup to unordered containers. By providing a transparent hash and Equality comparator, you can look up keys using a different type without constructing the key:
// Transparent hash [N4950 §22.14.3]
using is_transparent = void ;
auto operator () ( std :: string_view sv ) const noexcept {
return std :: hash < std :: string_view > {}(sv);
auto operator () ( const std :: string & s ) const noexcept {
return std :: hash < std :: string > {}(s);
// Transparent equality [N4950 §22.5.3.2]
struct TransparentEqual {
using is_transparent = void ;
template < typename T , typename U >
bool operator () ( const T & a , const U & b ) const noexcept {
std :: unordered_map < std :: string, int , TransparentHash, TransparentEqual > m;
// Lookup with string_view — no temporary std::string constructed
std :: string_view sv = " hello " ;
std :: cout << " Found: " << it -> first << " -> " << it -> second << " \n " ;
The is_transparent typedef signals to the container that the hash and equality functions accept Heterogeneous types. This enables find``count``containsAnd equal_range to work with any Type that is comparable to Key and hashable.
The standard provides the following guarantees [N4950 §22.4.4.4]:
insert / emplace : If the operation throws (e.g., key comparison throws during tree traversal, or the element constructor throws), the container is unchanged (strong guarantee).erase : Never throws (destructors are noexcept for standard types).extract : The node handle takes ownership of the extracted element. If the extraction fails (key not found), the container is unchanged.swap : Never throws (pointer swap only).clear : Never throws.For std::unordered_map``rehash provides the basic guarantee: if an exception occurs during Rehashing (e.g., hash computation throws), the container is in a valid but unspecified state [N4950 §22.5.5.3].
1. Mutable keys in std::set and std::map: The keys of ordered associative containers must Remain ordered at all times. The iterator types for std::set yield const Key&Preventing direct Mutation. However, std::map iterators yield std::pair<const Key, T>&And the value type is std::pair<const Key, T> — the key is const. This is by design: mutating a key violates the Ordering invariant and causes undefined behavior [N4950 §22.4.4.1].
2. operator[] default-inserts: m[key] inserts a default-constructed value if key is Absent, then returns a reference to it. This is surprising when used in a read-only context — it Silently modifies the container. Use m.at(key) (throws on miss) or m.find(key) (returns Iterator) for lookups that should not modify the map.
3. Reference stability in std::unordered_map: Rehashing invalidates all iterators, pointers, And references. If you store a reference or pointer to a value and then insert enough elements to Trigger rehash, that reference dangles. Use reserve() before inserting to prevent rehashing, or Store indices/keys instead of raw pointers.
4. Hash collision attacks on std::unordered_map: The default std::hash for strings is Deterministic but not cryptographically secure. An adversary who can control keys can craft inputs That all hash to the same bucket, degrading O ( 1 ) O(1) O ( 1 ) lookup to O ( n ) O(n) O ( n ) . In security-sensitive contexts (e.g., HTTP header parsing), use a hash-seeded or randomized hash function, or switch to std::map For guaranteed O ( log n ) O(\log n) O ( log n ) worst-case.
5. Forgetting to reserve() on unordered_map: The default initial bucket count is small ( 1 or a platform-specific minimum). Without reserve()The first few inserts trigger Repeated rehashing, each costing O ( n ) O(n) O ( n ) . For a map expected to hold n n n elements, call m.reserve(n) before inserting to pre-allocate buckets and avoid all intermediate rehashes [N4950 §22.5.5.3].
6. Using std::unordered_map when you need range queries: std::unordered_map provides no Ordering guarantee. Operations like lower_bound``upper_boundAnd equal_range in the ordered Sense are not available. If you need to iterate over all keys in a range [ l o , h i ] [lo, hi] [ l o , hi ] Use std::map Instead.
7. Erasing while iterating with operator[]: In std::mapThe pattern m.erase(m[key]) first Default-inserts key (via operator[]) and then immediately erases it, causing a redundant Allocation and deallocation. Use m.erase(m.find(key)) instead, which only erases if the key Exists.
When the default std::less<Key> is not suitable (e.g., case-insensitive string comparison), Provide a custom comparator:
struct CaseInsensitiveLess {
bool operator () ( std :: string a , std :: string b ) const {
std :: ranges :: transform (a, a. begin (), []( unsigned char c ) { return std :: tolower (c); });
std :: ranges :: transform (b, b. begin (), []( unsigned char c ) { return std :: tolower (c); });
std :: map < std :: string, int , CaseInsensitiveLess > m;
m[ " hello " ] = 3 ; // Replaces "Hello" (case-insensitive equal)
for ( const auto& [k, v] : m) {
std :: cout << k << " -> " << v << " \n " ;
The custom comparator must define a strict weak ordering : irreflexive (a < a a \lt a a < a is false), Asymmetric (a < b a \lt b a < b implies b < a b \lt a b < a is false), and transitive (a < b a \lt b a < b and b < c b \lt c b < c implies a < c a \lt c a < c ). Violating these invariants causes undefined behavior.
Understanding bucket distribution is critical for diagnosing performance problems. A high maximum Bucket size indicates hash collisions:
std :: unordered_map < std :: string, int> m;
for ( int i = 0 ; i < 1000 ; ++ i) {
m[ " key_ " + std :: to_string (i)] = i;
std :: cout << " bucket_count: " << m. bucket_count () << " \n " ;
std :: cout << " load_factor: " << m. load_factor () << " \n " ;
std :: cout << " max_load_factor: " << m. max_load_factor () << " \n " ;
// Find the largest bucket
std :: size_t max_bucket_size = 0 ;
std :: size_t max_bucket_idx = 0 ;
for ( std :: size_t i = 0 ; i < m. bucket_count (); ++ i) {
auto sz = m. bucket_size (i);
if (sz > max_bucket_size) {
std :: cout << " max bucket size: " << max_bucket_size
<< " (bucket " << max_bucket_idx << " ) \n " ;
// Ideal: max bucket size should be 1 or 2 for a good hash function
// If max bucket size is > 10, consider improving the hash function
For performance-critical code where ordered iteration is not needed, consider alternatives to std::unordered_map:
std::vector<std::pair<K,V>>> + std::sort + std::lower_bound : For small maps (fewer than ~100 elements), a sorted vector with binary search is faster than a hash table due to cache locality. Lookup is O ( log n ) O(\log n) O ( log n ) but with a very small constant.
boost::flat_map : A sorted vector adapter that provides map semantics. Excellent cache locality, but insertion is O ( n ) O(n) O ( n ) .
absl::flat_hash_map / absl::btree_map : Google’s Abseil library provides highly optimized hash maps with SIMD-accelerated probing and B-tree-based ordered maps.
tsl::hopscotch_map / tsl::robin_map : Open-addressing hash maps with better cache locality than std::unordered_map’s separate chaining.
Associative containers are like phone books: std::set is like a phone book sorted alphabetically — you can look up any name in O(log n) time, but you can’t have two people with the same name. std::map is like a phone book where each name has a phone number — it stores key-value pairs sorted by key. std::unordered_set is like a phone book organized by first letter — lookup is O(1) on average, but the entries aren’t sorted. std::unordered_map is the most commonly used — it’s a hash table with O(1) average lookup.
Why it matters: Associative containers provide O(log n) or O(1) lookup, compared to O(n) for std::vector. When you need to find elements by key (not by position), associative containers are the right choice. The choice between ordered (std::map) and unordered (std::unordered_map) depends on whether you need sorted iteration or maximum speed.
The key insight: std::unordered_map is the fastest lookup container (O(1) average), but std::map is better when you need sorted iteration or guaranteed O(log n) worst case.
Confusing authentication (who you are) with authorisation (what you can do) in security contexts.
Neglecting to normalise database designs, leading to data redundancy and update anomalies.
Writing pseudocode that is too language-specific rather than using standard algorithmic constructs.
Mixing up Big O, Big Ω \Omega Ω , and Big Θ \Theta Θ notation. Big O is an upper bound, not necessarily tight.
The key principles covered in this topic are linked in the sub-pages above. Focus on understanding the definitions, applying the formulas or frameworks, and evaluating strengths and limitations of each approach.
Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.