The C++ Textbook

Part 1 · Foundations

Repetition

Loops, and the off-by-one errors that live in them.

By the end of this chapter you can

  • Write a range-based for loop over a container
  • Convert an index loop to a range loop and back
  • Identify the exit condition that makes a loop terminate

C++ has four ways to repeat, and one of them should be your default. This chapter is about which, and about the small number of ways loops go wrong.

Range-based for: the default

If you are visiting every element of a container, this is the loop to write:

The loop with nothing to get wrong
#include <iostream>
#include <string>
#include <vector>

int main() {
    const std::vector<std::string> words{"alpha", "beta", "gamma"};

    for (const std::string& word : words) {
        std::cout << word << ' ';
    }
    std::cout << '\n';

    // It works on anything with begin and end — including a raw array.
    int numbers[] = {1, 2, 3, 4};
    int total = 0;
    for (int n : numbers) total += n;
    std::cout << "total " << total << '\n';
}

There is no index, so there is no off-by-one. There is no bound, so it cannot run past the end. It stops when the container stops.

Choose the loop variable’s form deliberately:

Three declarations, three meanings
#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> words{"alpha", "beta"};

    for (std::string w : words) w += "!";              // a copy: modifies nothing
    std::cout << "after by-value:     " << words[0] << '\n';

    for (std::string& w : words) w += "!";             // binds: modifies the element
    std::cout << "after by-reference: " << words[0] << '\n';

    for (const std::string& w : words) {               // binds, promises not to modify
        std::cout << "reading: " << w << '\n';
    }
}

const auto& should be your reflex for reading, auto& for modifying, and plain auto only for small types where a copy is genuinely free.

The index loop, and when you need it

When the index itself matters
#include <iostream>
#include <vector>

int main() {
    const std::vector<int> v{10, 20, 30};

    for (std::size_t i = 0; i < v.size(); ++i) {
        std::cout << i << ": " << v[i] << '\n';
    }
}

Reach for this when you need the position, when you are walking two containers in step, or when you are not visiting every element.

Three details in that header carry weight:

  • std::size_t, not int. size() returns an unsigned type, and comparing it against a signed int is a warning and a latent bug.
  • < v.size(), not <= v.size(). The last valid index is size() - 1.
  • ++i, not i++. For an int they are identical; for an iterator or a heavy type, i++ makes a copy to return the old value. The habit costs nothing and sometimes saves something.
The unsigned wraparound, in a loop
#include <iostream>
#include <vector>

int main() {
    std::vector<int> empty;

    std::cout << "empty.size() - 1 = " << empty.size() - 1 << '\n';

    // Bounded so the sample terminates; the real bug has no bound.
    for (std::size_t i = 0; i <= empty.size() - 1 && i < 3; ++i) {
        std::cout << "reading index " << i << ": " << empty[i] << '\n';
    }
}

while, and do-while

while repeats as long as a condition holds, and is right when the number of iterations is not known up front:

Looping until something happens
#include <iostream>

int main() {
    int value = 1000;
    int halvings = 0;

    while (value > 1) {
        value /= 2;
        ++halvings;
    }
    std::cout << "halved " << halvings << " times to reach " << value << '\n';
}

Every while needs an answer to one question: what changes each iteration to eventually make the condition false? Here it is value /= 2. If you cannot point at the line that makes progress, the loop does not terminate.

do/while runs the body once before testing, which is occasionally what you want for “prompt, then validate”:

do-while runs at least once
#include <iostream>
#include <sstream>

int main() {
    std::istringstream input{"-3 -1 7"};
    int value = 0;

    do {
        input >> value;
        std::cout << "read " << value << '\n';
    } while (value < 0 && input);

    std::cout << "first non-negative: " << value << '\n';
}

It is rare. When in doubt, use while — a loop whose body might need to run zero times is far more common than one that must always run once.

break and continue

Leaving early, and skipping ahead
#include <iostream>
#include <vector>

int main() {
    const std::vector<int> v{3, 8, 2, 9, 4};

    // break: stop entirely
    for (int x : v) {
        if (x > 5) {
            std::cout << "first over five: " << x << '\n';
            break;
        }
    }

    // continue: skip the rest of this iteration
    std::cout << "odd values: ";
    for (int x : v) {
        if (x % 2 == 0) continue;
        std::cout << x << ' ';
    }
    std::cout << '\n';
}

break leaves only the innermost loop. For nested loops, the usual answers are to extract the inner loop into a function and return, or to use a flag — C++ has no labelled break.

Escaping nested loops
#include <iostream>
#include <optional>
#include <vector>

// Extracting to a function makes `return` the escape.
std::optional<std::pair<int, int>> find_pair(const std::vector<int>& v, int target) {
    for (std::size_t i = 0; i < v.size(); ++i) {
        for (std::size_t j = i + 1; j < v.size(); ++j) {
            if (v[i] + v[j] == target) {
                return std::pair{static_cast<int>(i), static_cast<int>(j)};
            }
        }
    }
    return std::nullopt;
}

int main() {
    const std::vector<int> v{2, 7, 11, 15};

    if (auto found = find_pair(v, 9)) {
        std::cout << "indices " << found->first << " and " << found->second << '\n';
    }
    if (!find_pair(v, 100)) {
        std::cout << "no pair sums to 100\n";
    }
}

Modifying while looping

Do not change a container’s size while a range-based for is walking it. Chapter 4.4 explains why in terms of iterators; the short version is that the loop took its start and end points once, before the first iteration.

Growing a container mid-loop
#include <iostream>
#include <vector>

int main() {
    std::vector<int> v{1, 2, 3};

    for (int x : v) {
        if (x == 2) v.push_back(99);   // may reallocate: the loop's bounds are stale
        std::cout << x << ' ';
    }
    std::cout << '\n';
}

When you need to remove elements, say so directly:

Removing safely
#include <iostream>
#include <vector>

int main() {
    std::vector<int> v{1, 2, 3, 4, 5, 6};

    std::erase_if(v, [](int x) { return x % 2 == 0; });

    for (int x : v) std::cout << x << ' ';
    std::cout << '\n';
}

Prefer an algorithm when one fits

Many loops have a name. Chapter 4.5 covers them properly, but the habit starts here:

The same three loops, named
#include <algorithm>
#include <iostream>
#include <numeric>
#include <vector>

int main() {
    const std::vector<int> v{4, 8, 15, 16, 23, 42};

    std::cout << "sum:      " << std::accumulate(v.begin(), v.end(), 0) << '\n';
    std::cout << "largest:  " << *std::ranges::max_element(v) << '\n';
    std::cout << "any odd:  " << std::boolalpha
              << std::ranges::any_of(v, [](int x) { return x % 2; }) << '\n';
}

A loop you write can be wrong. std::ranges::max_element cannot.

Check yourself

Practice