std::unordered_map
std::unordered_map
containerIn 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.
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.
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};
}
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
empty()
MethodIf 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
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
[]
operatorThe 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.
at()
methodLike 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
find()
methodThe 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
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
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
.
insert()
methodWithin 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
insert_or_assign()
methodThe 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:
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
emplace()
methodThe 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
try_emplace()
methodThe 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.
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
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
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
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
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
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
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
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.
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
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:
std::hash
, or we need to provide a custom hash function for our map==
operator, or we need to provide a custom comparison functionIn 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
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.
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.
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
rehash()
FunctionThe 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
reserve()
MethodThe 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
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:
std::unordered_map
, including its creation and initialization with key-value pairs.size()
and empty()
.contains()
, count()
, the []
operator, at()
, and find()
methods.[]
operator, insert()
, insert_or_assign()
, emplace()
, and try_emplace()
.std::unordered_map
.erase()
and clear()
methods.extract()
method from C++17.std::hash
and operator==
.std::unordered_map
, including concepts like buckets, load factor, and methods like rehash()
and reserve()
.std::unordered_map
for specific scenarios.std::unordered_map
Creating hash maps using the standard library's std::unordered_map
container
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.