Hash Maps using std::unordered_map
Creating hash maps using the standard library's std::unordered_map
container
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.
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.
Using 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
Using 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 istrue
, our entry was inserted; if it isfalse
, 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, sotry_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
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);
}
};
}
Hashing and std::hash
This lesson provides an in-depth look at hashing in C++, including std::hash
, collision strategies, and usage in hash-based containers.
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.
Function Objects (Functors)
This lesson introduces function objects, or functors. This concept allows us to create objects that can be used as functions, including state management and parameter handling.
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:
Hash Sets using std::unordered_set
This lesson provides a thorough understanding of std::unordered_set
, from basic initialization to handling custom types and collisions
Using 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
Using 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
Using 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()
andempty()
. - Techniques for element access, including
contains()
,count()
, the[]
operator,at()
, andfind()
methods. - Methods for element insertion, such as using the
[]
operator,insert()
,insert_or_assign()
,emplace()
, andtry_emplace()
. - Handling of duplicate objects and understanding the uniqueness of keys in
std::unordered_map
. - Strategies for removing elements using
erase()
andclear()
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
andoperator==
. - 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 likerehash()
andreserve()
. - Comparisons with ordered maps and the benefits of using
std::unordered_map
for specific scenarios.
Nullable Values, std::optional
and Monadic Operations
A comprehensive guide to using std::optional
to represent values that may or may not be present.