Course outline · 0% complete

0/27 lessons0%

Course overview →

Iteration Patterns: Range-for, auto, and Iterators

lesson 8-3 · ~11 min · 22/27

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:

HeaderCompiles?Effect
for (std::string s : words)yesmodifies a copy that is discarded
for (const std::string& s : words)noassignment to s is rejected
for (std::string& s : words)yesmodifies the real elements
for (auto& s : words)yesidentical, 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.