Polyglot
Languages C++ loops
C++ § loops

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:

  1. The init-statement (an expression statement or a declaration), evaluated once before the loop.
  2. The condition, evaluated before each iteration; the loop terminates when it is false.
  3. 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.
  • auto when 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 for for 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 for only 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.