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:
#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:
#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
#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, notint.size()returns an unsigned type, and comparing it against a signedintis a warning and a latent bug.< v.size(), not<= v.size(). The last valid index issize() - 1.++i, noti++. For anintthey 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.
#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:
#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”:
#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
#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.
#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.
#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:
#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:
#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.