Loops
A loop is one of the fundamental constructs: it describes repetition. But in C++ that single word hides a whole set of different mechanisms with different semantics, different guarantees, and different trade-offs. Choosing between for, while, range-based for, and an algorithm from <algorithm> is not a matter of style — it is a matter of expressing intent correctly.
The classic for says: "I manage the counter." while says: "I wait for a condition." Range-based for says: "I traverse a range, I don't need the index." std::transform says: "this is a transformation, not a loop." The more precisely intent is expressed, the fewer mistakes you make and the more opportunities the compiler has to optimize.
for
The classic loop with an explicit counter:
for (initialization; condition; step) {
body
}
The three parts run like this: initialization — once before the first iteration; condition — before every iteration, exit if false; step — after each body.
for (int i = 0; i < 10; ++i) {
std::cout << i << " ";
}
// Output: 0 1 2 3 4 5 6 7 8 9
Prefer ++i over i++ for iterators and non-trivial types — the postfix increment creates a temporary copy. For int there is no difference, but the habit reads better.
All three parts are optional. An infinite loop:
for (;;) {
// runs until break or return
if (done) break;
}
Nested loops with several variables in the initializer (C++17):
for (int i = 0, j = 9; i < j; ++i, --j) {
std::cout << i << " " << j << "\n";
}
while
A loop with a precondition — the condition is checked before the body. If the condition is false initially, the body never runs.
int n = 1;
while (n < 100) {
n *= 2;
}
// n == 128
while is convenient when the number of iterations is unknown in advance and is driven by an external condition:
std::string line;
while (std::getline(std::cin, line)) {
process(line); // while the stream is open
}
do-while
A loop with a postcondition — the body runs at least once, the condition is checked afterwards:
int input;
do {
std::cout << "Enter a number from 1 to 10: ";
std::cin >> input;
} while (input < 1 || input > 10);
Used the least often. A good fit for retry logic and input loops where the first attempt is always needed.
Comparing loops
Range-based for (C++11)
The most readable way to traverse a container — when you don't need the index:
std::vector<int> v = {1, 2, 3, 4, 5};
for (int x : v) // a copy of each element
std::cout << x << " ";
for (int& x : v) // a reference — modify the original
x *= 2;
for (const int& x : v) // a const reference — read without copying
std::cout << x << " ";
for (auto& x : v) // recommended: auto deduces the type automatically
x += 10;
Rule: use auto& by default for modification, const auto& for reading heavy objects, and auto by value only for cheap primitives.
What the compiler generates
Range-based for is syntactic sugar. The compiler expands for (auto x : c) into:
{
auto&& __range = c;
auto __begin = begin(__range); // ADL: std::begin or the .begin() method
auto __end = end(__range); // ADL: std::end or the .end() method
for (; __begin != __end; ++__begin) {
auto x = *__begin;
// body
}
}
Range-for: what the compiler does
Structured bindings in range-for (C++17)
Range-based for pairs perfectly with structured bindings:
std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 82}};
for (const auto& [name, score] : scores) {
std::cout << name << ": " << score << "\n";
}
// A vector of pairs
std::vector<std::pair<int, std::string>> items = {{1, "one"}, {2, "two"}};
for (auto& [id, label] : items) {
label += "!"; // modify through the reference
}
break and continue
break — an immediate exit from the current loop:
for (int i = 0; i < 100; ++i) {
if (i * i > 50) {
break;
}
}
continue — skip the rest of the body and move to the next iteration:
for (int i = 0; i < 10; ++i) {
if (i % 2 == 0) continue; // skip the evens
std::cout << i << " "; // 1 3 5 7 9
}
Both statements work only with the nearest enclosing loop. To break out of nested loops, use a flag or extract into a function:
// A flag — readable, but adds a variable
bool found = false;
for (int i = 0; i < n && !found; ++i) {
for (int j = 0; j < m; ++j) {
if (grid[i][j] == target) {
found = true;
break;
}
}
}
// A lambda — cleaner, keeps locality
auto search = [&]() -> std::optional<std::pair<int,int>> {
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j)
if (grid[i][j] == target)
return {{i, j}};
return std::nullopt;
};
auto pos = search();
Common pitfalls
Comparing a signed and an unsigned counter
std::vector<int> v = {1, 2, 3};
// Warning: comparison of int and size_t (unsigned)
for (int i = 0; i < v.size(); ++i) { ... }
// Correct: an explicit cast or size_t
for (std::size_t i = 0; i < v.size(); ++i) { ... }
// Or: cast to int (if the vector is guaranteed to be small)
for (int i = 0; i < (int)v.size(); ++i) { ... }
When int i is -1 and v.size() returns a size_t, the comparison i < v.size() yields false — because -1, in its signed representation, becomes a huge number when converted to size_t.
A copy instead of a reference
std::vector<std::string> words = {"hello", "world"};
for (auto word : words) // copies each string — expensive
word += "!"; // and pointless: the original is not changed
for (auto& word : words) // a reference — modifies the original
word += "!";
Modifying the container during iteration
std::vector<int> v = {1, 2, 3, 4, 5};
// UNDEFINED BEHAVIOR — erase invalidates the iterator
// that range-for manages
for (auto x : v) {
if (x % 2 == 0) v.erase(/* ... */); // UB
}
// The correct idiomatic way: the erase-remove idiom
v.erase(std::remove_if(v.begin(), v.end(),
[](int x) { return x % 2 == 0; }),
v.end());
// C++20: std::erase_if
std::erase_if(v, [](int x) { return x % 2 == 0; });
Shadowing the loop variable
int i = 100;
for (int i = 0; i < 10; ++i) { // the outer i is hidden
std::cout << i; // 0..9, not 100
}
// The outer i is still 100, but readability suffered
Algorithms as a replacement for loops
<algorithm> contains high-level operations that eliminate whole classes of loop bugs — off-by-one, a forgotten break, incorrect accumulation.
#include <algorithm>
#include <numeric>
std::vector<int> nums = {3, 1, 4, 1, 5, 9, 2, 6};
// Sum — instead of an accumulation loop
int sum = std::accumulate(nums.begin(), nums.end(), 0);
// Find the first match
auto it = std::find(nums.begin(), nums.end(), 5);
if (it != nums.end())
std::cout << "Found at position " << std::distance(nums.begin(), it);
// Check a condition
bool has_negative = std::any_of(nums.begin(), nums.end(),
[](int x) { return x < 0; });
// Transform all elements
std::transform(nums.begin(), nums.end(), nums.begin(),
[](int x) { return x * 2; });
// Apply an action to each element
std::for_each(nums.begin(), nums.end(),
[](int& x) { x += 1; });
An algorithm conveys intent more precisely: std::accumulate is "a sum," not "a loop that sums." The compiler gets context for vectorization and optimization.
Performance: the loop body as the critical path
If a loop runs millions of times, its body is the hot path. A few principles:
Branch prediction. Branches inside a loop make it harder for the CPU to predict the next instruction. Where possible, remove conditions from the hot path — use [[likely]]/[[unlikely]] (C++20) as a hint to the compiler.
Vectorization. The compiler can replace the loop with SIMD instructions if the body has no non-analyzable dependencies. Avoid potentially aliasing pointers in the body — add __restrict__ or [[assume(...)]] when necessary.
Loop unrolling. The compiler can unroll the loop body, reducing the counter overhead. #pragma GCC unroll N or Clang attributes give an explicit hint.
// Example: a branch-free loop — friendly to vectorization
std::vector<float> a(N), b(N), c(N);
for (std::size_t i = 0; i < N; ++i)
c[i] = a[i] + b[i]; // the compiler turns this into SIMD at -O2
// A loop with a condition — worse for vectorization
for (std::size_t i = 0; i < N; ++i)
if (a[i] > 0) c[i] = a[i] + b[i]; // requires masking
Iterator invalidation and performance. std::vector guarantees contiguous memory — iterating over it is cache-friendly. std::list or std::map cause a cache miss on every iteration. If you need a fast traversal, std::vector is almost always better, even when insertion is more expensive.
Early exits and infinite loops
Early exits with standard algorithms:
// std::find stops at the first match
auto pos = std::find(v.begin(), v.end(), target);
// std::any_of stops at the first true
bool ok = std::any_of(v.begin(), v.end(), pred);
// std::all_of stops at the first false
bool all = std::all_of(v.begin(), v.end(), pred);
An infinite loop in a thread is a typical worker pattern:
#include <thread>
void worker() {
while (!stop_flag.load(std::memory_order_acquire)) {
if (!queue.empty()) {
process(queue.front());
queue.pop();
} else {
std::this_thread::yield(); // yield the time slice
}
}
}
std::this_thread::yield() tells the OS scheduler that the thread is willing to give up the CPU — useful in spin-wait loops. Without it, such a loop pins a core at 100%.
Interview relevance
Loops are one of the first topics in any C++ technical interview, but the interviewer isn't looking at your knowledge of syntax.
What they check:
- Whether you understand how range-based
forexpands into begin/end/increment/compare — and what follows from that - Whether you know the iterator-invalidation rules for specific containers
- Whether you use algorithms where they fit better than a loop
- Whether you see the difference between
for (auto x : v)(a copy) andfor (auto& x : v)(a reference) - Whether you understand the signed/unsigned counter comparison problem
Popular question directions:
- What happens if you call
erase()on an element inside a range-based for? — Expected answer: UB due to iterator invalidation; the correct way is the erase-remove idiom orstd::erase_if. - How does
for (auto x : v)differ fromfor (auto& x : v)— when do you use each? - How does begin/end lookup work in a range-based for — why does it work for raw arrays and user-defined types?
- When is
std::transformbetter than a hand-writtenforloop? - What is a sentinel type in C++20 and why is it needed for range-for?
A typical candidate mistake: removing elements from a container inside a range-based for — this is undefined behavior. Candidates often know it's "not allowed" but can't explain why: __end is computed before the loop starts, erase invalidates the iterators, and the next ++__begin walks into undefined memory.