Containers and Iterators
Apologies – this section has not been written yet.
Range-Based For Loops
A range-based for loop is a special syntax for iterating over sequences that support traversal by iterator. Rather than writing out each piece of the traversal, we can have the compiler generate it for us:
vector<int> vec = { 1, 2, 3, 4, 5 };
for (int item : vec) {
cout << item << endl;
}
The syntax of a range-based for loop is:
for (<type> <variable> : <sequence>)
<body>
In the example above, the variable is named item
and has type
int
, and vec
is the sequence over which the loop iterates. The
compiler automatically converts this into a traversal by iterator:
for (auto it = vec.begin(); it != vec.end(); ++it) {
int item = *it;
cout << item << endl;
}
The variable item
is initialized in each iteration from an element
in the sequence. Then the code in the body of the range-based for
loop follows.
The following loop attempts to set every element in the vector to the value 42:
for (int item : vec) {
item = 42; // attempt to set element to 42
}
However, this loop does not actually modify any elements. To understand why, let us take a look at its translation into a traversal by iterator:
for (auto it = vec.begin(); it != vec.end(); ++it) {
int item = *it;
item = 42;
}
The loop is actually modifying a copy of each element rather than an element itself. To avoid making a copy, we can declare the loop variable to be a reference:
for (int &item : vec) {
item = 42; // actually set element to 42
}
This translates to:
for (auto it = vec.begin(); it != vec.end(); ++it) {
int &item = *it;
item = 42;
}
This successfully modifies each element to have the value 42.
Range-based for loops are often used in combination with auto
, as
in the following:
vector<int> vec = { 1, 2, 3, 4, 5 };
for (auto &item : vec) {
item = 42;
}
for (auto item : vec) {
cout << item << endl;
}
The first loop declares item
as a reference, so that it aliases an
element in the sequence. The second loop does not declare item
as
a reference, so it produces a copy of an element in the sequence. The
following is the translation of both loops:
vector<int> vec = { 1, 2, 3, 4, 5 };
for (auto it = vec.begin(); it != vec.end(); ++it) {
int &item = *it; // alias
item = 42;
}
for (auto it = vec.begin(); it != vec.end(); ++it) {
int item = *it; // copy
item = 42;
}
With a range-based for loop, we can simplify print_all()
:
template <typename Sequence>
void print_all(const Sequence &sequence) {
for (auto &item : sequence) {
cout << item << endl;
}
}
We can call it on any sequence [1] that supports traversal by iterator, as long as the element type can be inserted into an output stream:
vector<string> vec = { "hello", "world!" };
print_all(vec);
Iterating over a Map
The standard-library map defines an iterator that produces key-value pairs upon dereference. The following is an example that constructs a map, counting how many times each unique word occurs in a vector. It then iterates over the map to print out the counts.
void print_word_counts(const std::vector<std::string> &words) {
std::map<std::string, int> word_counts;
// Each time a word is seen, add 1 to its entry in the map
for (const auto &word : words) {
word_counts[word] += 1;
}
// Print out the results by iterating through the map.
for (const auto &key_value : word_counts) { // key-value pairs
const auto &word = key_value.first;
const auto &count = key_value.second;
cout << word << "occurred " << count << " times." << endl;
}
}
When incrementing a word’s count, we do not need to check whether the word is in the map; if not, the map automatically inserts the word into the map with a value-initialized count of zero, which we then increment to one.
We use range-based for loops to iterate over both the vector and the
map, declaring a type-deduced reference to each element. A reference
avoids making a copy, which is nontrivial for strings. The iteration
over the map produces key-value pairs (std::pair<std::string,
int>
), and we access the first
and second
members to obtain
the word and count, respectively.