Loops (for, while, do-while)
Four looping forms cover every case. The decision is rarely about capability — they are mostly interchangeable — and mostly about intent: pick the form that makes the loop's contract obvious to the reader.
The four forms
for (int i = 0; i < n; ++i) { ... } // known/counted iteration; i scoped to the loop
while (cond) { ... } // zero-or-more: test BEFORE each pass
do { ... } while (cond); // one-or-more: test AFTER each pass
for (const auto& x : container) { ... } // range-based: "for each element"
| Form | Test timing | Reach for it when… |
|---|---|---|
for | before | the index/count is part of the logic |
while | before | iterations depend on a condition, not a count |
do-while | after | the body must run at least once (menus, retry) |
range-for | before | you just need each element, not the index |
Prefer range-based for
When you only need the elements, the range form removes the index, the bounds, and a whole class of off-by-one and iterator-invalidation bugs.
for (const auto& item : items) total += item.price; // read-only: const ref, no copy
for (auto& item : items) item.price *= 1.1; // mutate in place: non-const ref
for (auto [key, val] : table) use(key, val); // structured bindings over a map (C++17)
const auto&— read elements without copying (the default).auto&— modify elements in place.auto(by value) — only when you genuinely want a copy to mutate locally.
Plain auto over a container of heavy objects silently copies every element.
break, continue, and exit conditions
for (auto& job : queue) {
if (job.cancelled) continue; // skip to the next iteration
if (job.fatal) break; // leave the loop entirely
process(job);
}
break/continue affect only the innermost loop. To exit nested loops, prefer extracting the
nest into a function and return-ing, rather than reaching for goto.
Common pitfalls
Modifying a container while looping over it can invalidate the iterator or index you are using.
for (auto it = v.begin(); it != v.end(); ++it)
if (pred(*it)) v.erase(it); // BUG: erase invalidates it
v.erase(std::remove_if(v.begin(), v.end(), pred), v.end()); // erase–remove idiom: correct
for (size_t i = n - 1; i >= 0; --i) never terminates — an unsigned value is always >= 0, so it
wraps around instead of going negative. Loop with a signed index, or use iterators / i-- tricks.
Beyond hand-written loops
Many loops are really an algorithm in disguise. Expressing the intent is clearer and harder to get wrong than re-deriving the mechanics:
auto n = std::count_if(v.begin(), v.end(), is_even); // vs a manual counting loop
std::ranges::sort(v); // vs a hand-rolled sort (C++20)
See Algorithms and Ranges.
Summary
- Match the form to intent:
forfor counts,while/do-whilefor conditions, range-forfor elements. - Default the range loop variable to
const auto&; useauto&to mutate. break/continuetouch only the innermost loop.- Don't mutate a container's size while iterating it — use the erase–remove idiom.
- Reverse loops with unsigned counters underflow forever; use a signed index.
- If a loop is "count / find / transform / sort", an algorithm says it better.