본문으로 건너뛰기

Associative Containers

Associative Container: A container that stores and retrieves elements by key rather than by position.

Key: A value used to identify or locate an element.

The standard library provides eight associative containers.

ContainerMeaning
mapOrdered key-value pairs with unique keys
setOrdered unique keys
multimapOrdered key-value pairs allowing duplicate keys
multisetOrdered keys allowing duplicates
unordered_mapHashed key-value pairs with unique keys
unordered_setHashed unique keys
unordered_multimapHashed key-value pairs allowing duplicate keys
unordered_multisetHashed keys allowing duplicates

Ordered containers are declared in:

#include <map>
#include <set>

Unordered containers are declared in:

#include <unordered_map>
#include <unordered_set>

11.1 Using an Associative Container

A map stores key-value pairs.

A set stores keys only.

Using a map

map: An associative array whose elements are accessed by key.

#include <map>
#include <string>

std::map<std::string, std::size_t>
word_count;

Here:

key type = std::string
mapped type = std::size_t

A classic word-counting program is:

std::string word;

while (std::cin >> word)
{
++word_count[word];
}

If word is not already present, the subscript operation creates a new element whose mapped value is value-initialized.

For std::size_t, that initial value is:

0

Then the value is incremented.


Iterating through a map

A map element is a pair.

for (const auto& entry : word_count)
{
std::cout
<< entry.first
<< " occurs "
<< entry.second
<< " times\n";
}

For each map element:

first = key
second = mapped value

Ordered maps are traversed in key order.


Using a set

set: An associative container that stores unique keys.

A set is useful when the main question is:

Does this value exist?

Example:

std::set<std::string> excluded{
"the",
"but",
"and",
"or"
};

Check whether a word should be ignored:

if (excluded.find(word)
== excluded.end())
{
++word_count[word];
}

If find() returns end(), the key is not present.


11.2 Overview of the Associative Containers

Associative containers organize their elements according to keys rather than insertion position.

They do not provide positional operations such as:

push_front()
push_back()

Ordered associative containers provide bidirectional iterators.


1) Defining an Associative Container

Default Construction

std::map<std::string, int>
scores;

std::set<int>
values;

Both containers are initially empty.


List Initialization

std::set<int> values{
1,
2,
3,
4
};

A map initializer contains key-value pairs.

std::map<std::string, int> scores{
{"Alice", 95},
{"Bob", 88},
{"Carol", 91}
};

Range Initialization

std::vector<int> source{
1,
2,
2,
3,
3
};

std::set<int> values(
source.begin(),
source.end()
);

The set contains:

1 2 3

Duplicate values are ignored because set requires unique keys.


Unique-Key Containers

These containers permit at most one element for each key:

map
set
unordered_map
unordered_set

Example:

std::set<int> values{
1,
1,
2,
2,
3
};

The resulting set contains:

1 2 3

Multiple-Key Containers

These containers allow equivalent keys to occur multiple times:

multimap
multiset
unordered_multimap
unordered_multiset

Example:

std::multiset<int> values{
1,
1,
2,
2,
3
};

The container keeps all five elements.


2) Requirements on Key Type

Ordered associative containers need a way to order their keys.

By default they use:

operator<

The comparison must establish a strict weak ordering.


Key Equivalence

Two keys are considered equivalent when neither is less than the other.

Conceptually:

!compare(a, b)
&&
!compare(b, a)

For a unique-key ordered container, equivalent keys represent the same key.


Custom Comparison Function

A custom comparator can replace the default < operation.

Suppose:

struct Record
{
int id;
std::string name;
};

Comparator:

bool compare_record(
const Record& lhs,
const Record& rhs)
{
return lhs.id < rhs.id;
}

Define a set using the function pointer type:

using Compare =
bool (*)(
const Record&,
const Record&
);

std::set<
Record,
Compare
> records(compare_record);

The comparator type is part of the container type.

The comparator object is supplied when the container object is created.


Lambda Comparator

A callable object can also provide ordering.

auto compare =
[](const Record& lhs,
const Record& rhs)
{
return lhs.id < rhs.id;
};

std::set<
Record,
decltype(compare)
> records(compare);

The comparison must remain consistent while elements are stored in the container.


3) The pair Type

pair: A library template that stores two public members.

Required header:

#include <utility>

Example:

std::pair<std::string, int>
item{
"Alice",
95
};

Access the members:

std::cout << item.first;
std::cout << item.second;

Constructing a pair

std::pair<std::string, int>
p1;

std::pair<std::string, int>
p2("Alice", 95);

std::pair<std::string, int>
p3{"Bob", 88};

Using make_pair():

auto p4 =
std::make_pair(
std::string("Carol"),
91
);

The member types are inferred.


Returning a pair

A function may return a pair directly.

std::pair<std::string, int>
make_score()
{
return {
"Alice",
95
};
}

Usage:

auto result =
make_score();

std::cout
<< result.first
<< ' '
<< result.second;

11.3 Operations on Associative Containers

Associative containers define type aliases specific to key-based storage.

TypeMeaning
key_typeKey type
mapped_typeMapped value type; map types only
value_typeStored element type

For:

std::map<std::string, int>

the types are conceptually:

key_type
std::string

mapped_type
int

value_type
std::pair<const std::string, int>

The map key is const because changing it in place would break the container's ordering.


1) Associative Container Iterators

Dereferencing a map iterator gives a reference to its value_type.

std::map<std::string, int>
scores{
{"Alice", 95},
{"Bob", 88}
};

auto iter =
scores.begin();

Access the key:

std::cout
<< iter->first;

Access the mapped value:

std::cout
<< iter->second;

Map Keys Cannot Be Modified

This is invalid:

// Error:
// iter->first = "Carol";

The key is const.

The mapped value can be changed:

iter->second = 100;

Set Iterators Are Read-Only

For a set:

std::set<int> values{
1,
2,
3
};

auto iter =
values.begin();

The element may be read:

std::cout << *iter;

but not changed:

// Error:
// *iter = 10;

Changing a key directly could violate the ordering of the container.


Ordered Traversal

Ordered associative containers iterate in key order.

std::map<std::string, int> scores{
{"Carol", 91},
{"Alice", 95},
{"Bob", 88}
};

for (const auto& entry : scores)
{
std::cout
<< entry.first
<< '\n';
}

Output:

Alice
Bob
Carol

Associative Containers and Generic Algorithms

Read-only algorithms can work with associative-container iterators.

However, algorithms that modify keys or reorder elements are generally inappropriate.

For lookup, prefer the container member:

scores.find("Alice");

rather than a generic linear search.

The member operation can use the container's internal search structure.


2) Adding Elements

Important operations include:

insert()
emplace()

Inserting into a set

std::set<int> values;

values.insert(10);
values.insert(20);
values.insert(10);

The second insertion of 10 has no effect because a set has unique keys.


Inserting into a map

A map element is a key-value pair.

std::map<std::string, int>
scores;

scores.insert(
{"Alice", 95}
);

Other forms include:

scores.insert(
std::make_pair(
"Bob",
88
)
);

or:

scores.insert(
std::pair<
std::string,
int
>{
"Carol",
91
}
);

Return Value from Unique-Key insert()

For map and set, inserting one element returns:

std::pair<iterator, bool>

Example:

auto result =
scores.insert(
{"Alice", 95}
);

Check whether insertion occurred:

if (result.second)
{
std::cout
<< "inserted\n";
}
else
{
std::cout
<< "key already exists\n";
}

The iterator:

result.first

refers to the element with that key.


Word Counting with insert()

Instead of subscripting:

++word_count[word];

the insertion return value can be used:

auto result =
word_count.insert(
{word, 1}
);

if (!result.second)
{
++result.first->second;
}

If the key did not exist, {word, 1} is inserted.

If it already existed, the existing counter is incremented.


Inserting into a Multi Container

A multimap or multiset allows duplicate keys.

std::multimap<
std::string,
std::string
> authors;

authors.insert(
{"Barth", "Sot-Weed Factor"}
);

authors.insert(
{"Barth", "Lost in the Funhouse"}
);

Both elements are stored.

Single-element insertion into a multi container returns an iterator to the inserted element.


emplace()

emplace() constructs an element directly inside the container.

std::map<std::string, int>
scores;

scores.emplace(
"Alice",
95
);

The arguments are used to construct the stored pair.


3) Erasing Elements

Associative containers support several forms of erase().

Erase by Key

std::set<int> values{
1,
2,
3
};

auto removed =
values.erase(2);

Result:

removed = 1

For a unique-key container, the result is normally:

0 or 1

Erase from a Multi Container

std::multiset<int> values{
1,
2,
2,
2,
3
};

auto removed =
values.erase(2);

Result:

removed = 3

Every equivalent key is erased.


Erase by Iterator

auto iter =
values.find(3);

if (iter != values.end())
{
values.erase(iter);
}

Only the element denoted by the iterator is removed.


Erase a Range

values.erase(
values.begin(),
values.end()
);

This removes the specified iterator range.


4) Subscripting a map

Only:

map
unordered_map

support subscripting.

set, multimap, and multiset do not.


Map Subscript

std::map<std::string, int>
scores;

scores["Alice"] = 95;

The expression:

scores["Alice"]

returns the mapped value associated with "Alice".


Missing-Key Insertion

If the key does not exist:

int score =
scores["Bob"];

a new element is inserted.

Conceptually:

key = "Bob"
value = 0

because int is value-initialized.

This side effect is important.


Why Word Counting Is Concise

++word_count[word];

works because a missing key automatically creates a counter initialized to zero.

Conceptually:

missing word

insert {word, 0}

increment

{word, 1}

at()

at() accesses an existing mapped value without inserting a new element.

int score =
scores.at("Alice");

If the key does not exist, at() throws:

std::out_of_range

Example:

try
{
std::cout
<< scores.at("Unknown");
}
catch (
const std::out_of_range&)
{
std::cout
<< "key not found\n";
}

Subscript Return Type

For:

std::map<std::string, int>
scores;

this:

scores["Alice"]

has the mapped type:

int

as an lvalue.

Therefore it can be modified:

++scores["Alice"];

By contrast, dereferencing a map iterator yields:

pair<const string, int>

5) Accessing Elements

Important lookup operations include:

OperationMeaning
find(k)Find first element with key k
count(k)Count elements with key k
lower_bound(k)First key not less than k
upper_bound(k)First key greater than k
equal_range(k)Range of elements equivalent to k

find()

auto iter =
scores.find("Alice");

if (iter != scores.end())
{
std::cout
<< iter->second;
}

If the key does not exist:

iter == scores.end()

Use find() for Noninserting Lookup

Do not use:

scores["Unknown"];

when the intention is only to check whether the key exists.

That expression can insert a new element.

Prefer:

if (scores.find("Unknown")
!= scores.end())
{
// key exists
}

count()

For unique-key containers:

scores.count("Alice");

returns:

0 or 1

For multi containers it can return larger values.

authors.count("Barth");

might return:

2

Finding All Elements in a multimap

Because equal keys are adjacent in an ordered multi container, one approach is:

auto count =
authors.count("Barth");

auto iter =
authors.find("Barth");

while (count)
{
std::cout
<< iter->second
<< '\n';

++iter;
--count;
}

lower_bound() and upper_bound()

For an ordered associative container:

auto begin =
authors.lower_bound("Barth");

auto end =
authors.upper_bound("Barth");

The range:

[begin, end)

contains all elements whose key is equivalent to "Barth".

Traversal:

for (auto iter = begin;
iter != end;
++iter)
{
std::cout
<< iter->second
<< '\n';
}

If there are no matching keys:

begin == end

equal_range()

equal_range() returns both iterators together.

auto range =
authors.equal_range("Barth");

Equivalent range:

[range.first, range.second)

Traversal:

for (auto iter = range.first;
iter != range.second;
++iter)
{
std::cout
<< iter->second
<< '\n';
}

This is often the clearest way to process all elements with an equivalent key.


6) A Word Transformation Map

A useful associative-container example is a word transformation program.

A map stores:

original word -> replacement text

Example:

std::map<
std::string,
std::string
> transformations{
{"brb", "be right back"},
{"k", "okay"},
{"u", "you"}
};

Building the Transformation Map

Suppose each rule contains:

key replacement text

Using subscript assignment:

transformations[key] =
replacement;

If the key is new, a new element is inserted.

If the key already exists, its mapped value is replaced.


Nonmodifying Transformation Lookup

Use find() because looking up a word should not add a new rule.

const std::string&
transform(
const std::string& word,
const std::map<
std::string,
std::string
>& rules)
{
auto iter =
rules.find(word);

if (iter != rules.cend())
{
return iter->second;
}

return word;
}

If the word exists:

return replacement

Otherwise:

return original word

Why Not Use operator[]?

Using:

rules[word]

would be inappropriate for a const map and, on a non-const map, could insert missing words.

Lookup should not modify the transformation table.


11.4 The Unordered Containers

Unordered Associative Container: An associative container organized by hashing rather than key ordering.

Examples:

std::unordered_map<
std::string,
int
> scores;

std::unordered_set<
std::string
> words;

Ordered vs. Unordered

Ordered containers use a comparison operation.

map / set

key comparison

ordered traversal

Unordered containers use:

hash function
+
key equality
```

and do not maintain sorted key order.

---

### Hash-Based Lookup

Conceptually:

~~~text
key

hash function

hash value

bucket

compare keys inside bucket

The hash function determines which bucket should contain the element.

The equality operation distinguishes equivalent keys within that bucket.


Basic Use

std::unordered_map<
std::string,
std::size_t
> word_count;

std::string word;

while (std::cin >> word)
{
++word_count[word];
}

The interface is very similar to map.

Operations such as:

find()
insert()
erase()
count()
operator[]
```

are also available where applicable.

---

### Traversal Order

Do not rely on iteration order in an unordered container.

~~~cpp
for (const auto& entry
: word_count)
{
std::cout
<< entry.first
<< '\n';
}

The output is not guaranteed to be alphabetically ordered.


When to Use Unordered Containers

Unordered containers are useful when:

  • key ordering is not needed
  • hashing is available for the key type
  • average-case hash lookup is desirable

Ordered containers are often simpler when:

  • sorted traversal is useful
  • range operations such as lower_bound() are needed
  • a natural key ordering already exists

Buckets

An unordered container stores elements in buckets.

Different keys may hash to the same bucket.

This is a collision.

The container resolves collisions by comparing keys inside the selected bucket.


Bucket Operations

OperationMeaning
bucket_count()Current number of buckets
max_bucket_count()Maximum possible bucket count
bucket_size(n)Number of elements in bucket n
bucket(k)Bucket containing key k
load_factor()Average elements per bucket
max_load_factor()Maximum target load factor
rehash(n)Reorganize using an appropriate bucket count
reserve(n)Prepare for at least n elements

bucket_count()

auto count =
word_count.bucket_count();

This reports the current number of buckets.


bucket_size()

for (std::size_t i = 0;
i != word_count.bucket_count();
++i)
{
std::cout
<< "bucket "
<< i
<< ": "
<< word_count.bucket_size(i)
<< '\n';
}

This can be used to inspect the hash distribution.


bucket()

auto index =
word_count.bucket("hello");

index identifies the bucket associated with the key.


Load Factor

Load Factor: The average number of elements per bucket.

Conceptually:

load factor
=
number of elements
------------------
number of buckets

Retrieve it with:

auto load =
word_count.load_factor();

A high load factor can increase the number of keys that must be examined inside a bucket.


rehash()

word_count.rehash(100);

requests that the container reorganize its elements using enough buckets to satisfy the requested bucket count and load-factor requirements.

Rehashing can invalidate iterators.


reserve()

word_count.reserve(1000);

prepares the container to hold approximately the requested number of elements without needing an immediate rehash, subject to the container's load-factor rules.

Use it when the expected number of elements is known in advance.


Requirements on Key Types

For an unordered container, the key type needs:

  1. a hash operation
  2. an equality operation

The standard library provides:

std::hash<T>

for many built-in and standard-library types.

Example:

std::unordered_set<std::string>
names;

std::hash<std::string> supplies the default hash function.


Key Equality

By default, equivalent keys are compared using equality.

Conceptually:

lhs == rhs

Equal keys must produce equal hash values.


Custom Hash Function

Suppose:

struct Point
{
int x;
int y;
};

Equality:

bool operator==(
const Point& lhs,
const Point& rhs)
{
return
lhs.x == rhs.x
&&
lhs.y == rhs.y;
}

Hasher:

struct PointHash
{
std::size_t operator()(
const Point& point) const
{
auto h1 =
std::hash<int>{}(
point.x
);

auto h2 =
std::hash<int>{}(
point.y
);

return h1 ^ (h2 << 1);
}
};

Use it as:

std::unordered_set<
Point,
PointHash
> points;

The unordered container uses:

PointHash
to choose a bucket

operator==
to test key equality

Custom Equality Operation

A separate equality callable can also be supplied.

struct PointEqual
{
bool operator()(
const Point& lhs,
const Point& rhs) const
{
return
lhs.x == rhs.x
&&
lhs.y == rhs.y;
}
};

Container:

std::unordered_set<
Point,
PointHash,
PointEqual
> points;

The hash and equality functions must agree:

if a == b
then hash(a) == hash(b)

Essential Study Checklist

  1. Associative containers store and retrieve elements by key.
  2. map stores key-value pairs.
  3. set stores keys.
  4. map and set require unique keys.
  5. multimap and multiset allow duplicate keys.
  6. Containers beginning with unordered_ use hashing instead of key ordering.
  7. map is often called an associative array.
  8. Map subscripts use keys rather than numeric positions.
  9. A map element is a pair whose first member is the key and second is the mapped value.
  10. Subscripting a missing map key inserts a value-initialized mapped value.
  11. set is useful for membership tests.
  12. Ordered associative containers iterate in key order.
  13. Ordered associative containers use a comparison operation on keys.
  14. The default ordered comparison is normally <.
  15. Ordered key comparison must establish a strict weak ordering.
  16. Equivalent ordered keys satisfy neither a < b nor b < a under the comparator.
  17. A custom comparator type becomes part of the container type.
  18. pair stores two public members named first and second.
  19. make_pair() creates a pair while deducing its member types.
  20. key_type is the associative container's key type.
  21. mapped_type is the associated value type for map containers.
  22. A map's value_type is pair<const key_type, mapped_type>.
  23. Map keys cannot be changed through iterators.
  24. A map's mapped value can be modified through a non-const iterator.
  25. Set iterators provide read-only access to keys.
  26. Prefer associative-container member lookup operations over generic linear search.
  27. insert() adds elements to associative containers.
  28. A map insertion value is a key-value pair.
  29. Inserting an existing key into map or set does not add another element.
  30. Single-element insertion into a unique-key container returns pair<iterator, bool>.
  31. The returned bool indicates whether insertion occurred.
  32. multimap and multiset allow repeated equivalent keys.
  33. erase(k) removes every element whose key is equivalent to k.
  34. In a unique-key container, erase(k) returns 0 or 1.
  35. In a multi container, erase(k) may return a larger count.
  36. Only map and unordered_map support subscripting.
  37. operator[] can insert a missing key.
  38. Use find() when lookup must not insert a new element.
  39. at() accesses an existing mapped value and throws if the key is absent.
  40. find(k) returns an iterator to a matching element or end().
  41. count(k) returns the number of elements with key k.
  42. lower_bound(k) returns the first key not less than k.
  43. upper_bound(k) returns the first key greater than k.
  44. [lower_bound(k), upper_bound(k)) contains all equivalent ordered keys.
  45. equal_range(k) returns the same equivalent-key range as a pair of iterators.
  46. A word-transformation map naturally represents word -> replacement rules.
  47. Use find() in nonmodifying transformation lookup.
  48. Unordered containers use a hash function and key equality.
  49. Unordered containers do not maintain sorted traversal order.
  50. A hash function maps keys to integral hash values.
  51. Hash values are used to select buckets.
  52. Keys that hash to the same bucket cause collisions.
  53. bucket_count() reports the number of buckets.
  54. bucket_size(n) reports the number of elements in a bucket.
  55. load_factor() is the average number of elements per bucket.
  56. rehash() reorganizes the bucket structure.
  57. reserve() prepares an unordered container for an expected number of elements.
  58. std::hash<T> supplies default hashing for supported key types.
  59. Custom key types may require custom hashing and equality operations.
  60. Equal keys must always produce equal hash values.