Loops
C++ provides four loop forms: the for loop inherited from C, the range-based for introduced in C++11, while, and do … while. Range-based for is the conventional form for iterating over containers, ranges, and arrays in modern C++; the C-style for retains its place where the loop index is genuinely needed or where the iteration is non-trivial. Loops in C++ interact closely with iterators, ranges, and the algorithms library; explicit loops are common in user code, but a substantial fraction of iteration in well-written C++ is expressed through the algorithms (std::for_each, std::transform, std::copy_if) and ranges (std::ranges::for_each, the views in <ranges>) rather than as raw loops.
while
The while loop tests its controlling expression before each iteration:
while (!queue.empty()) {
auto job = queue.dequeue();
process(job);
}
int c;
while ((c = std::cin.get()) != EOF) {
handle_byte(static_cast<unsigned char>(c));
}
The construct is the appropriate one for iteration whose termination condition is checked at the start, and where the number of iterations is not known in advance. Reading from a stream, polling for state, and graph traversal are typical.
do … while
The do … while loop tests its controlling expression after each iteration; the body always executes at least once:
int input;
do {
std::print("> ");
std::cin >> input;
} while (input < 0 || input > 100);
The construct is appropriate for iterations whose first execution is unconditional. It appears less frequently than while; the principal idioms are input prompts, retry loops, and certain numerical methods whose termination criterion depends on the result of the iteration.
The terminating semicolon after while (cond) is a syntactic peculiarity worth remembering; it is the only compound statement in C++ that requires one.
The C-style for
The C-style for loop combines initialisation, test, and step in one header:
for (std::size_t i = 0; i < n; ++i) {
process(data[i]);
}
The header has three parts:
- The init-statement (an expression statement or a declaration), evaluated once before the loop.
- The condition, evaluated before each iteration; the loop terminates when it is
false. - The iteration-expression, evaluated after each iteration, before the next condition test.
Any of the three may be omitted; for (;;) is the conventional infinite loop.
for (;;) {
auto event = wait_for_event();
if (event.kind == EVENT_QUIT) break;
handle(event);
}
The init-statement may be a declaration (since C99 and C++98); the variable is scoped to the loop:
for (auto it = container.begin(); it != container.end(); ++it) {
use(*it);
}
// it is not in scope here
The convention ++i versus i++ is partly habitual — they are equivalent for built-in int — and partly substantive: for non-trivial iterator types, ++i avoids constructing and discarding an intermediate value.
for with multiple variables
The comma operator permits multiple expressions in the init-statement and the iteration-expression:
for (std::size_t i = 0, j = n - 1; i < j; ++i, --j) {
std::swap(data[i], data[j]);
}
For multi-variable loops with declarations, structured bindings on a pair-like type are an alternative:
for (auto [i, j] = std::pair{0u, n - 1}; i < j; ++i, --j) {
/* ... */
}
Range-based for
C++11 introduced the range-based for, the conventional form for iterating over containers, arrays, and any object that exposes begin()/end() (or for which non-member begin/end are found via ADL):
std::vector<int> values = {1, 2, 3, 4, 5};
for (int v : values) {
std::cout << v << ' ';
}
The element variable may be declared with any conventional declaration form:
for (auto v : values) { /* v is a copy */ }
for (auto &v : values) { /* v is a reference; modifications affect the container */ }
for (const auto &v : values) { /* v is a const reference; cheap, read-only */ }
for (auto &&v : values) { /* v is a forwarding reference; conventional in templates */ }
The conventional choice:
const auto &for read-only iteration over an arbitrary type — avoids copies for non-trivial elements.auto &when the body modifies the elements.autowhen the element type is small and the body needs an independent copy.auto &&in templates, where the value category of the source range is unknown.
The range may be any expression that yields a range-like value — a container, an array, a std::initializer_list, a custom type, or (since C++20) a view:
for (int v : std::vector{1, 2, 3, 4, 5}) { /* ... */ } // initialiser list
for (int v : {1, 2, 3, 4, 5}) { /* ... */ } // braced list
#include <ranges>
for (int v : values | std::views::filter([](int x) { return x > 2; })) {
/* iterates 3, 4, 5 */
}
C++20 added the init-statement to the range-based form:
for (auto data = load_data(); const auto &row : data) {
process(row);
}
The pattern keeps the source of the range scoped to the loop.
Iterating with iterators
When the index or position is needed and the range-based form does not suffice, explicit iterators are the alternative:
for (auto it = container.begin(); it != container.end(); ++it) {
if (should_remove(*it)) {
it = container.erase(it);
if (it == container.end()) break;
} else {
++it;
}
}
The pattern — increment in one branch, do not in the other — is the conventional way to iterate while removing elements. erase returns the iterator following the removed element, which the loop must respect.
For C++20, std::ranges provides convenience functions and a richer iterator/sentinel model:
auto it = std::ranges::find(container, target);
if (it != container.end()) {
/* found */
}
break and continue
break exits the innermost enclosing for, while, do, or switch:
for (const auto &row : matrix) {
for (int v : row) {
if (v == target) {
found = true;
break; // exits the inner for only
}
}
}
continue skips the rest of the current iteration and proceeds to the next test:
for (int v : values) {
if (v < 0) continue;
process(v);
}
C++ does not have multi-level break or continue. Exiting a nested loop requires a flag, a goto, or a refactoring into a function whose return exits both:
auto find_in_matrix = [&](int target) -> std::optional<std::pair<int, int>> {
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < cols; ++j) {
if (matrix[i][j] == target) return std::pair{i, j};
}
}
return std::nullopt;
};
if (auto pos = find_in_matrix(42)) {
auto [row, col] = *pos;
/* ... */
}
The function form is the conventional alternative; the goto-and-label form is rare in C++ because RAII makes the cleanup discipline of the C goto-cleanup pattern unnecessary.
The algorithms library as an alternative
A substantial fraction of explicit loops in C++ code can be replaced by calls into <algorithm> and <numeric>. The trade-off: more declarative, less direct, often clearer:
// Explicit loop:
int total = 0;
for (int v : values) total += v;
// Algorithm:
auto total = std::accumulate(values.begin(), values.end(), 0);
// Range-based (C++20):
auto total = std::ranges::fold_left(values, 0, std::plus{});
// Explicit loop:
std::vector<int> doubled;
doubled.reserve(values.size());
for (int v : values) doubled.push_back(v * 2);
// Algorithm:
std::vector<int> doubled;
doubled.reserve(values.size());
std::transform(values.begin(), values.end(), std::back_inserter(doubled),
[](int v) { return v * 2; });
// Ranges (C++20):
auto doubled = values | std::views::transform([](int v) { return v * 2; })
| std::ranges::to<std::vector>();
The conventional contemporary advice:
- Use range-based
forfor ordinary iteration where the element is the principal subject. - Use algorithms when the operation has a well-known shape (
accumulate,transform,find,partition). - Use ranges and views (C++20) for composable pipelines.
- Use the C-style
foronly when the index or non-trivial step is essential.
Common iteration patterns
Bounded iteration
for (std::size_t i = 0; i < n; ++i) { /* uses i and v[i] */ }
Index and value together
// Pre-C++20:
for (std::size_t i = 0; i < values.size(); ++i) {
process(i, values[i]);
}
// With ranges (C++23):
for (auto [i, v] : values | std::views::enumerate) {
process(i, v);
}
Iterating two ranges in parallel
auto it1 = a.begin(), it2 = b.begin();
while (it1 != a.end() && it2 != b.end()) {
process(*it1, *it2);
++it1; ++it2;
}
// With ranges (C++23):
for (auto [x, y] : std::views::zip(a, b)) {
process(x, y);
}
Iterating a map’s keys or values
for (const auto &[key, value] : my_map) {
/* structured binding */
}
// With ranges:
for (const auto &k : my_map | std::views::keys) { /* ... */ }
for (const auto &v : my_map | std::views::values) { /* ... */ }
Reading lines from a stream
std::string line;
while (std::getline(std::cin, line)) {
process_line(line);
}
std::getline reads up to the next newline (excluding it); the loop terminates on EOF or stream error. The form is the C++ conventional substitute for C’s fgets-driven loop.