One way to walk anything
This lesson exists because the STL's core design idea, algorithms that work on any container, needs a common currency, and iterators are that currency.
Learn the loop styles here and unit 9's entire algorithm library opens up with no new syntax to absorb.
Range-based for: Python's for-in, in C++
Since C++11, iterating a container looks like this:
std::vector<int> v = {1, 2, 3}; for (int x : v) { // x is a COPY of each element std::cout << x; } for (int& x : v) { // x is a REFERENCE: modify the real elements x *= 2; } for (const std::string& s : words) { // big elements: const& avoids copies std::cout << s; }
The value and reference distinction is lesson 4-2 all over again, applied once per element. Use copies for cheap reads, & to mutate, and const& for large read-only elements.
auto
auto lets the compiler deduce a variable's type from its initializer, so for (auto& x : v) keeps working if the element type of v later changes. Use it where the type is obvious from context, and write the type out where naming it teaches the reader something.
Iterators, briefly
Range-for is sugar over iterators, which are pointer-like objects where v.begin() refers to the first element and v.end() refers one past the last. The pointer walk from lesson 5-2 has exactly this shape, and every STL algorithm in unit 9 takes a begin and end pair.
Copy versus reference, one element at a time
Two loops with identical bodies over the same vector. The first cannot change it and the second does.
#include <iostream> #include <vector> int main() { std::vector<int> v = {1, 2, 3}; for (int x : v) { // copy: original untouched x *= 10; } std::cout << "after copy loop: " << v[0] << " " << v[1] << " " << v[2] << "\n"; for (int& x : v) { // reference: modifies v x *= 10; } std::cout << "after ref loop: " << v[0] << " " << v[1] << " " << v[2] << "\n"; return 0; }
Output
after copy loop: 1 2 3 after ref loop: 10 20 30
The single & is the whole difference, and this is the same lesson as tryToDouble versus doubleIt in lesson 4-2. In the first loop, x is a fresh copy on each iteration, multiplied and then discarded, so the vector never sees the change.
Compilers usually warn about the first loop, since a value computed into a variable that is never read afterwards is almost always a mistake. That warning is worth having enabled, because the loop is otherwise perfectly silent about doing nothing.
The explicit iterator loop
What the range-based for compiles down to, written out by hand.
#include <iostream> #include <string> #include <vector> int main() { std::vector<std::string> words = {"one", "two", "three"}; for (auto it = words.begin(); it != words.end(); ++it) { std::cout << *it << "\n"; // dereference, like a pointer } return 0; }
Output
one two three
auto is hiding a genuine mouthful here, since the real type is std::vector<std::string>::iterator. Writing it out is legal and almost nobody does it, which is the clearest case for auto in the language.
The loop shape should look familiar. it != words.end() is the same one-past-the-end comparison as p != a + 5 in lesson 5-2, and *it dereferences like a pointer. That similarity is not a coincidence, because a std::vector iterator often is a pointer underneath.
The pre-increment ++it rather than it++ is the conventional choice. For a plain pointer the two are identical in cost, but for heavier iterator types it++ has to copy the old value in order to return it, so ++it is the habit that never costs anything.
Choosing a loop header for in-place modification
Lowercasing every string in a std::vector<std::string> in place requires the header for (std::string& s : words).
In-place modification needs a non-const reference to each element, which is what that & provides. The two wrong answers fail in different ways, and the difference is worth being precise about:
| Header | Compiles? | Effect |
|---|---|---|
for (std::string s : words) | yes | modifies a copy that is discarded |
for (const std::string& s : words) | no | assignment to s is rejected |
for (std::string& s : words) | yes | modifies the real elements |
for (auto& s : words) | yes | identical, with the type deduced |
The by-value version is the dangerous one, because it compiles and runs while doing nothing. The const& version at least fails loudly at the point of the bug, which is a good argument for reaching for const& by default and removing the const only when mutation is intended.
This mirrors pass-by-reference for functions in lesson 4-2, and the reasoning transfers directly: use a copy for cheap reads, const& for expensive reads, and & when the element must change.
Three loops over one vector
Reading, squaring in place, and summing, with each loop choosing a copy or a reference according to what it needs.
#include <iostream> #include <vector> int main() { std::vector<int> v(5); for (int& x : v) { std::cin >> x; } for (int& x : v) { x = x * x; } int total = 0; for (int x : v) { total += x; } std::cout << "sum of squares: " << total << "\n"; return 0; }
Input
1 2 3 4 5
Output
sum of squares: 55
The first two loops take int& because both write to the elements, since reading into a copy with std::cin >> x would discard every value read. The third takes a plain int because summing only reads, and copying an int is free.
Reading with a range-based for only works because v(5) pre-sized the vector, so there are five elements to iterate over. An empty vector would produce a loop body that never runs, and the program would sit waiting for nothing.
The answer 55 is 1 + 4 + 9 + 16 + 25, and it is worth noticing that it matches the sum of 1 through 10 from lesson 3-3 by pure coincidence.
Squaring is written as x = x * x rather than x *= x, which is identical in effect. Both are fine, and unit 9's std::transform offers a third way to express the same pass.