· 2 min read ·

Why Your For-Loop Is Probably Fine, Until It Isn't

Source: isocpp

Andrzej Krzemieński published a piece on structured iteration back in December, and it’s worth revisiting. The core argument is familiar but worth restating clearly: index-based for-loops carry a class of bugs that more structured alternatives simply eliminate.

The classic form is something like this:

for (int i = 0; i < v.size(); ++i) {
    process(v[i]);
}

This works, mostly. But there are several ways it can go wrong. The comparison between a signed int and the unsigned result of size() is a latent warning waiting to become a bug in edge cases. Forgetting to use < instead of <= is an off-by-one that compilers won’t catch. And if you copy-paste this loop and change the container name in the body but not the condition, you’ve introduced a subtle mismatch that passes review.

The range-based for-loop, introduced in C++11, removes most of this:

for (auto& item : v) {
    process(item);
}

No index arithmetic, no signed/unsigned comparison, no possibility of mis-stating the bounds. The loop is structurally coupled to the container. That coupling is the point.

The tradeoffs are real, though. You lose the index, which matters when the position itself is meaningful. You can work around that with std::views::enumerate from C++23, or by maintaining a separate counter, but neither is as clean as an index loop when what you actually want is i.

For transformations and filtering, the C++20 ranges library pushes this further:

auto results = v
    | std::views::filter([](int x) { return x > 0; })
    | std::views::transform([](int x) { return x * 2; });

The intent is explicit in the structure. There is no loop body to misread, no condition to get wrong. The composition of operations reads left to right.

The underlying shift here is from imperative to declarative style. An index loop describes how to iterate. A range pipeline describes what to produce. Both are valid, but the declarative form tends to be harder to write incorrectly, because the language is doing more of the bookkeeping.

This doesn’t mean you should eliminate index loops from your codebase. When you need the index, use it. When you need to iterate over two containers simultaneously by position, a range-based for-loop won’t help you. C++23 adds std::views::zip for that case, but toolchain support is still catching up in a lot of environments.

The practical takeaway from Krzemieński’s piece is modest but important: prefer the more constrained form when the constrained form expresses what you mean. A range-based for-loop signals that you’re iterating over every element of a collection in order, and nothing else. That signal has value to the reader, and the reduced surface area for bugs has value to the author.

C++ has been accreting these safer iteration tools steadily since C++11, and the progression from index loops to range-for to ranges algorithms is worth understanding as a coherent design direction, not just a grab-bag of features. Each step moves some correctness responsibility from the programmer to the compiler or library.

Was this interesting?