Hash Maps using std::unordered_map

Creating hash maps using the standard library's std::unordered_map container
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

In previous lessons, we introduced the idea of hashing and std::pair. By combining these ideas, we can create hash maps, among the most useful data structures we’ll encounter.

Every element in a hash map is a std::pair, of two related values, whose type we are free to set.

The first object in each pair is called the key. Alongside each key is the second object in our pair, called the value.

The key is used as an input to the hash function, which determines where exactly our pair gets inserted. Because of this, if we have a key, we can retrieve the value associated with that key in fast, constant time.

Associative Arrays and Dictionaries

Because the primary operation we perform on hash maps is providing the key to retrieve the value associated with it, map data structures are sometimes called associative arrays or dictionaries in other programming languages.

Creating Maps

The C++ standard library’s implementation of a hash map is std::unordered_map. This is available by including the <unordered_map> header:

#include <unordered_map>

We create a std::unordered_map by providing two template parameters - the type we want to use for each key, and the type we want to use for each value, respectively. Below, we create a map where both the key and value will be a std::string:

#include <unordered_map>
#include <string>

int main() {
  using std::unordered_map, std::string;
  unordered_map<string, string> Map;
}

We can provide a collection of key-value pairs with which to initialize our map:

#include <unordered_map>
#include <string>

int main() {
  using std::unordered_map, std::string;

  unordered_map<string, string> Map{
    {"Bob", "robert@example.com"},
    {"Anna", "anna@example.com"},
    {"Dave", "dave@example.com"},
  };
}

Given each entry in a map is a std::pair, if we have such objects, we can also initialize a map from them:

#include <unordered_map>
#include <string>

int main() {
  using std::string;
  using Person = std::pair<string, string>;

  Person Anna{"Anna", "anna@example.com"};
  Person Bob{"Bob", "robert@example.com"};
  Person Dave{"Dave", "dave@example.com"};

  std::unordered_map<string, string> Map{
    Anna, Bob, Dave
  };
}

We can also use class template argument deduction (CTAD) as usual, to let the compiler deduce the type:

#include <unordered_map>
#include <string>

int main() {
  using namespace std::string_literals;
  std::pair Anna{"Anna"s, "anna@example.com"s};
  std::pair Bob{"Bob"s, "robert@example.com"s};
  std::pair Dave{"Dave"s, "dave@example.com"s};

  std::unordered_map Map{Anna, Bob, Dave};
}

Map Size

Maps have the size() method, which will return how many elements are currently stored:

#include <iostream>
#include <unordered_map>

int main() {
  using std::string;
  std::unordered_map<string, string> Map{
    {"Anna", "anna@example.com"},
    {"Bob", "robert@example.com"},
    {"Dave", "dave@example.com"}
  };

  std::cout << "Size: " << Map.size();
}
Size: 3

The empty() Method

If we explicitly want to know whether the map is empty or not - that is, where its size is 0 - we can use the more descriptive empty() method:

#include <iostream>
#include <unordered_map>

int main() {
  std::unordered_map<int, bool> Map;

  if (Map.empty()) {
    std::cout << "The map is empty";
  }
}
The map is empty

Element Access

There are several ways we can retrieve elements from our unordered map. They all rely on us having the key of the key-value pair we want to retrieve.

contains()

By passing a key to the contains() method, we create a bool representing whether or not our map contains that key:

#include <iostream>
#include <unordered_map>

int main() {
  using std::string;
  std::unordered_map<string, string> Map{
    {"Anna", "anna@example.com"},
    {"Bob", "robert@example.com"},
    {"Dave", "dave@example.com"}
  };

  if (Map.contains("Bob")) {
    std::cout << "The map has Bob";
  }

  if (!Map.contains("Steve")) {
    std::cout << ", but not Steve";
  }
}
The map has Bob, but not Steve

count()

The contains() method was only added in C++20. Before that, we used the less intuitive count() method, which returns how many entries match the key we provide.

Given a std::unordered_map cannot contain duplicates, count() will return either 0 or 1.

When coerced to a boolean, 0 and 1 return false and true respectively, so we can use count() in the same way we use contains():

#include <iostream>
#include <unordered_map>

int main() {
  using std::string;
  std::unordered_map<string, string> Map{
    {"Anna", "anna@example.com"},
    {"Bob", "robert@example.com"},
    {"Dave", "dave@example.com"}
  };

  if (Map.count("Bob")) {
    std::cout << "The map has Bob";
  }

  if (!Map.count("Steve")) {
    std::cout << ", but not Steve";
  }
}
The map has Bob, but not Steve

The [] operator

The primary way we access elements within an unordered map is through the [] operator. We provide a key, and the associated value is returned by reference:

#include <iostream>
#include <unordered_map>

int main() {
  using std::string;
  std::unordered_map<string, string> Map{
    {"Anna", "anna@example.com"},
    {"Bob", "robert@example.com"},
    {"Dave", "dave@example.com"}
  };

  std::cout << "Bob: " << Map["Bob"];
}
Bob: robert@example.com

If the key does not exist within our map, the [] operator will create a new entry. The entry will use this key, and default-construct a corresponding value:

#include <iostream>
#include <unordered_map>

struct SomeType {
  SomeType() {
    std::cout << "Default Constructing";
  }
};

int main() {
  std::unordered_map<std::string, SomeType> Map;
  Map["Hello"];
}
Default Constructing

If we do not want the behavior, we can first check if our map contains() the key we are about to access, or we can use the at() or find() methods instead.

The at() method

Like the [] operator, the at() method will return the value associated with the key we provide. The main difference is that, if the key does not exist in our map, at() will throw a std::out_of_range exception:

#include <iostream>
#include <unordered_map>

int main() {
  using std::string;
  std::unordered_map<string, string> Map{
    {"Anna", "anna@example.com"},
    {"Bob", "robert@example.com"},
    {"Dave", "dave@example.com"}
  };

  std::cout << "Bob: " << Map.at("Bob");

  try {
    Map.at("Steve");
  } catch (std::out_of_range& e) {
    std::cout << "\nSteve is not in the map:\n"
              << e.what();
  }
}
Bob: robert@example.com
Steve is not in the map:
invalid unordered_map<K, T> key

The find() method

The find() method returns an iterator to the key-value pair associated with the key we provide:

#include <iostream>
#include <unordered_map>

int main() {
  using std::string;
  std::unordered_map<string, string> Map{
    {"Anna", "anna@example.com"},
    {"Bob", "robert@example.com"},
    {"Dave", "dave@example.com"}
  };

  auto it{Map.find("Bob")};

  std::cout << "Key: " << it->first
            << "\nValue: " << it->second;
}
Key: Bob
Value: robert@example.com

If the map doesn’t contain the key, find() will return a past-the-end iterator for our map. We can test for this by comparing it to the iterator returned by our map’s end() method:

#include <iostream>
#include <unordered_map>

int main() {
  using std::string;
  std::unordered_map<string, string> Map{
    {"Anna", "anna@example.com"},
    {"Bob", "robert@example.com"},
    {"Dave", "dave@example.com"}
  };

  auto it{Map.find("Steve")};

  if (it == Map.end()) {
    std::cout << "Could not find Steve";
  } else {
    auto [key, value]{*it};
    std::cout << "Key: " << key
          << "\nValue: " << value;
  }
}
Could not find Steve

Element Insertion

We can combine the [] and = operators to insert a new object:

#include <iostream>
#include <unordered_map>

int main() {
  using std::string;
  std::unordered_map<string, string> Map{
    {"Anna", "anna@example.com"},
    {"Bob", "robert@example.com"},
    {"Dave", "dave@example.com"}
  };

  Map["Steve"] = "steve@example.com";

  std::cout << "Steve: " << Map["Steve"];
}
Steve: steve@example.com

Duplicate Objects

Note, that it’s not possible to have duplicate keys in a std::unordered_map. If we use the = operator with a key that is already used, we will instead be updating that key:

#include <iostream>
#include <unordered_map>

int main() {
  using std::string;
  std::unordered_map<string, string> Map{
    {"Anna", "anna@example.com"},
    {"Bob", "robert@example.com"},
    {"Dave", "dave@example.com"}
  };

  std::cout << "Size: " << Map.size();
  Map["Bob"] = "new-bob@example.com";
  std::cout << "\nSize: Still " << Map.size();
  std::cout << "\nBob: " << Map["Bob"];
}
Size: 3
Size: Still 3
Bob: new-bob@example.com

However, we can have duplicate values stored under different keys:

#include <iostream>
#include <unordered_map>

int main() {
  using std::string;
  std::unordered_map<string, string> Map{
    {"Anna", "anna@example.com"},
    {"Bob", "robert@example.com"},
    {"Dave", "dave@example.com"}
  };

  std::cout << "Size: " << Map.size();
  Map["Robert"] = "robert@example.com";
  std::cout << "\nSize: " << Map.size();

  if (Map["Bob"] == Map["Robert"]) {
    std::cout << "\nBob and Robert have the"
      " same value";
  }
}
Size: 3
Size: 4
Bob and Robert have the same value

A variation of std::unordered_map that supports duplicate keys is available - std::unordered_multimap.

The insert() method

Within a std::unordered_map, each pair of elements is a std::pair with the corresponding type.

For example, every element in a std::unordered_map<int, bool> is a std::pair<int, bool>.

If we have such a pair, we can add it directly using the insert() method:

#include <iostream>
#include <unordered_map>

int main() {
  using std::string;
  std::unordered_map<string, string> Map{
    {"Anna", "anna@example.com"},
    {"Bob", "robert@example.com"},
    {"Dave", "dave@example.com"}
  };

  std::pair<string, string> Steve {
    "Steve", "steve@example.com"};

  Map.insert(Steve);

  std::cout << "Steve: " << Map["Steve"];
}
Steve: steve@example.com

The insert_or_assign() method

The insert_or_assign() method gives us another way to add a new object, or update an existing one.

The main benefit of insert_or_assign() is that it returns a std::pair that lets us understand the effect of the function. The pair contains:

  • An iterator to where the new or updated entry is
  • A bool represents whether an insertion took place. If the boolean is true, our entry was inserted; if it is false, an existing entry was updated:
#include <iostream>
#include <unordered_map>

int main() {
  using std::string;
  std::unordered_map<string, string> Map{
      {"Bob", "robert@example.com"},
      {"Anna", "anna@example.com"},
      {"Dave", "dave@example.com"},
  };

  auto [it, wasInserted]{Map.insert_or_assign(
    "Bob", "bob@example.com")};

  if (!wasInserted) {
    std::cout << it->first << " already existed"
              << " - its value is now "
              << it->second;
  }
}
Bob already existed - its value is now bob@example.com

The emplace() method

The emplace() method allows us to construct a std::pair in place, directly into the map. This is more performant than creating the pair outside of the container, and then moving and copying it in, so should be preferred where possible.

Arguments provided to emplace() are forwarded to the underlying std::pair constructor:

#include <iostream>
#include <unordered_map>

int main() {
  using std::string;
  std::unordered_map<string, string> Map{
    {"Anna", "anna@example.com"},
    {"Bob", "robert@example.com"},
    {"Dave", "dave@example.com"}
  };

  Map.emplace("Steve", "steve@example.com");

  std::cout << "Steve: " << Map["Steve"];
}
Steve: steve@example.com

The try_emplace() method

The try_emplace() method will emplace our std::pair into the map, but only if the map doesn’t already contain an entry with a matching key.

Below, try_emplace() adds "Steve" because our container doesn’t yet have an entry with a key of "Steve". But it does not insert "Bob", because "Bob" already exists:

#include <iostream>
#include <unordered_map>

int main() {
  using std::string;
  std::unordered_map<string, string> Map{
    {"Anna", "anna@example.com"},
    {"Bob", "robert@example.com"},
    {"Dave", "dave@example.com"}
  };

  Map.try_emplace("Steve", "steve@example.com");
  Map.try_emplace("Bob", "rob@example.com");

  std::cout << "Steve: " << Map["Steve"];
  std::cout << "\nBob: " << Map["Bob"];
}
Steve: steve@example.com
Bob: robert@example.com

The try_emplace() method returns a std::pair containing an iterator and a boolean.

  • The iterator points to where the object is within the map
  • The boolean represents whether or not the insertion was successful. If the boolean is false, that means an entry with that key already existed, so try_emplace() did not create a new entry
#include <iostream>
#include <unordered_map>

int main() {
  using std::string;
  std::unordered_map<string, string> Map{
    {"Anna", "anna@example.com"},
    {"Bob", "robert@example.com"},
    {"Dave", "dave@example.com"}
  };

  auto[it, wasSuccessful]{
    Map.try_emplace("Bob", "rob@example.com")};

  if (!wasSuccessful) {
    std::cout << "The container already had"
                 " the key " << it->first
              << "\nValue: " << it->second;
  }
}
The container already had the key Bob
Value: robert@example.com

Removing Elements from a Map

Maps have the erase() method, which will remove the element with the specific key.

#include <iostream>
#include <unordered_map>

int main() {
  using std::string;
  std::unordered_map<string, string> Map{
    {"Anna", "anna@example.com"},
    {"Bob", "robert@example.com"},
    {"Dave", "dave@example.com"}
  };

  std::cout << "Size: " << Map.size();
  Map.erase("Bob");
  std::cout << "\nSize: " << Map.size();
}
Size: 3
Size: 2

They also have the clear() method, which will remove all the elements, leaving an empty map with a size of 0:

#include <iostream>
#include <unordered_map>

int main() {
  using std::string;
  std::unordered_map<string, string> Map{
    {"Anna", "anna@example.com"},
    {"Bob", "robert@example.com"},
    {"Dave", "dave@example.com"}
  };

  std::cout << "Size: " << Map.size();
  Map.clear();
  std::cout << "\nSize: " << Map.size();
}
Size: 3
Size: 0

Editing Values

When we have a key, we can update the value associated with that key in all the normal ways. For example, we can assign a new value using the = operator.

We can also modify the current value by calling operators or methods on it, or by passing it off to some other function by reference or pointer:

#include <iostream>
#include <unordered_map>

void Double(int& x) { x *= 2; }

int main() {
  using std::string;
  std::unordered_map<string, int> Map{
    {"a", 1},
    {"b", 2},
    {"c", 3}
  };

  Map["a"] = 100;
  ++Map["b"];
  Double(Map["c"]);

  std::cout << std::format("a={}, b={}, c={}",
    Map["a"], Map["b"], Map["c"]);
}
a=100, b=3, c=6

Editing Keys

The keys we use are processed by the container’s hash function to determine where our object should be inserted. Because of this, we can’t directly modify them, as doing so would leave them in the incorrect position within the underlying array.

We could erase the existing key-value pair and insert a new one. However, this approach can be problematic, as destroying the existing objects might have side effects, or recreating them might be expensive.

As of C++17, there is a more efficient way to accomplish this, using the extract() method. This method removes the node from the container, leaving our original objects in place.

The extracted node provides editable references to the key and value using the key() and mapped() methods respectively:

#include <iostream>
#include <unordered_map>

int main() {
  using std::string;
  std::unordered_map<string, string> Map {
      {"Bob", "robert@example.com"},
      {"Anna", "anna@example.com"},
      {"Dave", "dave@example.com"},
  };

  auto Node {Map.extract("Bob")};

  std::cout << "Extracted Key: " << Node.key()
            << ", Value: " << Node.mapped();
}
Extracted Key: Bob, Value: robert@example.com

From here, we can modify our node as needed and then insert it back into our map, which ensures our updated key is rehashed. To insert our node, we use an insert() overload that accepts an rvalue reference:

#include <iostream>
#include <unordered_map>

int main() {
  using std::string;
  std::unordered_map<string, string> Map {
      {"Bob", "robert@example.com"},
      {"Anna", "anna@example.com"},
      {"Dave", "dave@example.com"},
  };

  auto Node {Map.extract("Bob")};
  Node.key() = "Robert";
  Map.insert(std::move(Node));

  std::cout << "Robert: " << Map["Robert"];
}
Robert: robert@example.com

Map Iteration

The standard library implementation of maps supports iterators, so we can use the same techniques we introduced with other containers. The most common way we’ll iterate through a collection is using a range-based for loop.

Given each entry is a std::pair, we can access the key and value using the first and second member variables respectively:

#include <iostream>
#include <unordered_map>

int main() {
  using std::string;
  std::unordered_map<string, string> Map{
    {"Anna", "anna@example.com"},
    {"Bob", "robert@example.com"},
    {"Dave", "dave@example.com"}
  };

  for (auto Pair : Map) {
    std::cout << "\nKey: " << Pair.first;
    std::cout << ", Value: " << Pair.second;
  }
}
Key: Anna, Value: anna@example.com
Key: Bob, Value: robert@example.com
Key: Dave, Value: dave@example.com

Passing by Reference

Each element of the pair gets passed to the loop body by value. We will often want to switch that to pass-by-reference instead. As with functions, we can do that by adding an & to the type.

Given we are not changing it within the for loop, we can also mark it as const:

#include <iostream>
#include <unordered_map>

int main() {
  using std::string;
  std::unordered_map<string, string> Map{
    {"Anna", "anna@example.com"},
    {"Bob", "robert@example.com"},
    {"Dave", "dave@example.com"}
  };

  for (const auto& Pair : Map) {
    std::cout << "\nKey: " << Pair.first;
    std::cout << ", Value: " << Pair.second;
  }
}
Key: Anna, Value: anna@example.com
Key: Bob, Value: robert@example.com
Key: Dave, Value: dave@example.com

Structured Binding

Finally, we could also switch to using structured binding with the std::pair we receive. This removes the need to have the intermediate variable in our loop heading, and also allows us to give first and second more meaningful names:

#include <iostream>
#include <unordered_map>

int main() {
  using std::string;
  std::unordered_map<string, string> Map{
    {"Anna", "anna@example.com"},
    {"Bob", "robert@example.com"},
    {"Dave", "dave@example.com"}
  };

  for (const auto& [key, value] : Map) {
    std::cout << "\nKey: " << key;
    std::cout << ", Value: " << value;
  }
}
Key: Anna, Value: anna@example.com
Key: Bob, Value: robert@example.com
Key: Dave, Value: dave@example.com

Ordering

As the name suggests, a std::unordered_map is not ordered. That means, when we iterate over it, we can’t easily predict which order our elements will be accessed in.

An ordered map is available in the standard library as std::map. Rather than using hash-based techniques, std::map relies on a binary tree instead.

This approach allows elements to be stored in a predictable order, at a small cost to insertion and search performance. We cover trees and std::map in detail later in the course.

Using Custom Types

Naturally, we can store user-defined types within our unordered maps. In many use cases, our maps will contain a known set of keys, so it’s fairly common that we’ll be using enum types as our keys:

#include <iostream>
#include <unordered_map>

enum class Slot {Weapon, Armor, Helmet};

struct Item {
  std::string Name;
};

int main() {
  std::unordered_map<Slot, Item> Equipment{
    {Slot::Weapon, Item{"Iron Sword"}},
    {Slot::Armor,  Item{"Leather Armor"}},
    {Slot::Helmet, Item{"Wooden Bucket"}},
  };

  std::cout << "Helmet: "
    << Equipment[Slot::Helmet].Name;
}
Helmet: Wooden Bucket

Using Other Custom Types

We can use any user-defined type within maps, not just enum types. However, the type we use as a key has similar requirements to what we saw earlier when we introduced hash sets.  Specifically:

  • The key type needs to implement std::hash, or we need to provide a custom hash function for our map
  • The key type needs to be able to compare its objects using the == operator, or we need to provide a custom comparison function

In our introduction to this chapter, we walked through how to implement std::hash and operator==() for a custom type:

// User.h
#pragma once
#include <string>

struct User {
  std::string Name;
  bool operator==(const User& Other) const {
    return Name == Other.Name;
  }
};

namespace std {
template <>
struct hash<User> {
  size_t operator()(const User& U) const {
    return std::hash<std::string>{}(U.Name);
  }
};
}

The constraints on the value type are much looser. We can use almost any type although, outside of some advanced use cases, many std::unordered_map operators and methods require this type to be default constructible:

#include <string>
#include <unordered_map>

struct SomeType {
  SomeType() = delete;
  SomeType(int){};
};

int main() {
  std::unordered_map<std::string, SomeType> Map;
  Map["Three"] = 3;
}
error: 'SomeType::SomeType(void)': attempting to reference a deleted function

Using a Custom Hash Function

If the type we want to use as the key to our map does not implement std::hash, or it does but we want to customize the hash function anyway, we do that by passing an additional template argument when creating our map:

#include <unordered_map>
#include <iostream>

struct User {/*...*/} struct Hash { size_t operator()(const User& U) const { return std::hash<std::string>{}(U.Name); } }; int main() { std::unordered_map<User, std::string, Hash> M; M[User("Bob")] = "robert@example.com"; std::cout << "Bob: " << M[User("Bob")]; }
Bob: robert@example.com

As we saw in the earlier std::unordered_set lesson, the hasher template argument expects a type with which it can create function objects, or functors. We cover these in more detail later in the course.

Using a Custom Comparison Function

Two different objects can hash to the same value so, like all hash-based containers, std::unordered_map additionally needs a way to determine if two keys are unequivocally equal.

By default, it uses the key type’s == operator to determine this, but we can override this by providing a custom functor type as the fourth template argument:

#include <unordered_map>
#include <iostream>

struct User {
  std::string Name;
};

struct Hasher {/*...*/} struct Comparer { bool operator()( const User& A, const User& B) const { return A.Name == B.Name; } }; int main() { std::unordered_map< User, std::string, Hasher, Comparer> M; M[User("Bob")] = "robert@example.com"; std::cout << "Bob: " << M[User("Bob")]; }
Bob: robert@example.com

Remember, if two objects are the same, they should hash to the same value. That is, if Comparer(A, B) is true, then Hasher(A) must return the same value as Hasher(B).

The onus is on us to ensure our implementation satisfies this constraint. If it doesn’t, the std::unordered_map will behave unpredictably.

Buckets, Load Factor, and Rehashing

The std::unordered_map handles collisions in much the same way as a std::unordered_set, and gives us the same suite of methods to inspect and control that process.

We’ll quickly review them here, but more thorough explanations are available in the std::unordered_set lesson:

bucket_count()

The size of the primary array our std::unordered_map is currently using is available through the bucket_count() method:

#include <iostream>
#include <unordered_map>

int main() {
  using std::string;
  std::unordered_map<string, string> Map {
      {"Bob", "robert@example.com"},
      {"Anna", "anna@example.com"},
      {"Dave", "dave@example.com"},
  };

  std::cout << "Buckets: " << Map.bucket_count();
}
Buckets: 8

load_factor()

The load_factor() method returns the average number of objects per bucket. Our map contains 3 entries across 8 buckets in this example, so our load factor is 3/8:

#include <iostream>
#include <unordered_map>

int main() {
  using std::string;
  std::unordered_map<string, string> Map {
      {"Bob", "robert@example.com"},
      {"Anna", "anna@example.com"},
      {"Dave", "dave@example.com"},
  };

  std::cout << "Load Factor: "
    << Map.load_factor();
}
Load Factor: 0.375

max_load_factor()

When our load factor exceeds the maximum load factor, our container will resize and rehash. The max_load_factor() method is how we control this.

If we pass no arguments, the method returns the currently configured max load factor. If we provide a float, we can set a new value:

#include <iostream>
#include <unordered_map>

int main() {
  using std::string;
  std::unordered_map<string, string> Map {
      {"Bob", "robert@example.com"},
      {"Anna", "anna@example.com"},
      {"Dave", "dave@example.com"},
  };

  std::cout << "Current Max Load Factor: "
    << Map.max_load_factor();
  std::cout << "\nCurrent Load Factor: "
    << Map.load_factor();

  Map.max_load_factor(0.25);

  std::cout << "\nNew Max Load Factor: "
    << Map.max_load_factor();

  Map["Steve"] = "steve@example.com";
  std::cout << "\nNew Load Factor: "
    << Map.load_factor();
}
Current Max Load Factor: 1
Current Load Factor: 0.375
New Max Load Factor: 0.25
New Load Factor: 0.0625

The rehash() Function

The rehash() function accepts an integer argument. It then updates our container to use at least this many buckets, and rehashes objects to make use of the new space:

#include <iostream>
#include <unordered_map>

int main() {
  using std::string;
  std::unordered_map<string, string> Map {
      {"Bob", "robert@example.com"},
      {"Anna", "anna@example.com"},
      {"Dave", "dave@example.com"},
  };

  std::cout << "Current Bucket Count: "
    << Map.bucket_count();

  Map.rehash(500);

  std::cout << "\nNew Bucket Count: "
    << Map.bucket_count();
}
Current Bucket Count: 8
New Bucket Count: 512

The reserve() Method

The reserve() method works similarly to rehash(), except it takes into consideration our maximum load factor. Below, we reserve enough space for 500 entries, using a max load factor of 0.25.

As such, our container rehashes to at least 2,000 buckets, that is 500 / 0.25:

#include <iostream>
#include <unordered_map>

int main() {
  using std::string;
  std::unordered_map<string, string> Map {
      {"Bob", "robert@example.com"},
      {"Anna", "anna@example.com"},
      {"Dave", "dave@example.com"},
  };

  std::cout << "Current Bucket Count: "
    << Map.bucket_count();

  Map.max_load_factor(0.25);
  Map.reserve(500);

  std::cout << "\nNew Bucket Count: "
    << Map.bucket_count();
}
Current Bucket Count: 8
New Bucket Count: 2048

Summary

In this comprehensive lesson, we explored the functionalities and use-cases of std::unordered_map, covering its creation, manipulation, and advanced features like custom hash functions and handling custom types.

We delved into its various methods and properties. The key takeaways included:

  • Introduction to std::unordered_map, including its creation and initialization with key-value pairs.
  • Understanding of map size methods like size() and empty().
  • Techniques for element access, including contains(), count(), the [] operator, at(), and find() methods.
  • Methods for element insertion, such as using the [] operator, insert(), insert_or_assign(), emplace(), and try_emplace().
  • Handling of duplicate objects and understanding the uniqueness of keys in std::unordered_map.
  • Strategies for removing elements using erase() and clear() methods.
  • Approaches to editing values and the limitations in directly modifying keys.
  • Efficient key modification using the extract() method from C++17.
  • Techniques for iterating over maps using range-based for loops and structured bindings.
  • Insight into using custom types as keys, including requirements for std::hash and operator==.
  • Implementing custom hash and comparison functions for more complex key types.
  • Understanding the internal workings of std::unordered_map, including concepts like buckets, load factor, and methods like rehash() and reserve().
  • Comparisons with ordered maps and the benefits of using std::unordered_map for specific scenarios.

Was this lesson useful?

Next Lesson

Nullable Values using std::optional

A comprehensive guide to using std::optional to represent values that may or may not be present.
Illustration representing computer hardware
Ryan McCombe
Ryan McCombe
Updated
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Hash Sets and Hash Maps
Next Lesson

Nullable Values using std::optional

A comprehensive guide to using std::optional to represent values that may or may not be present.
Illustration representing computer hardware
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved