C++ Part I · Module 5 · Control Flow
for Loops
Chapter 5.4's failure mode was a missing progress statement — a loop whose body never moved the program toward its condition becoming false. The for loop makes that mistake hard to make, because it puts the initialization, the test and the advance side by side on one line where a reader can check all three at once. That is a real improvement and it is not free. What a for loop usually does is count, and counting correctly means getting both ends of a range right: how many iterations, starting where, stopping just before or just after. The language gives no help with this. Every bound is arithmetic you did, and the compiler will run whatever you wrote. The chapter spends most of its length on one convention that makes the arithmetic reliable, and then on the loop that breaks worst — a countdown whose index was declared with the wrong type. It printed three correct values, then four billion, and kept going. There was no warning, and the flag that would have caught it is not one the curriculum has been using.
Foundation16 min readC++Part IFundamentalsControl Flow
Part I · Module 5 · Chapter 5.5 · for Loops
1. Three Parts on One Line
A for loop collects the three pieces of a counting loop into its header:
for (int i = 0; i < 4; ++i) {
std::cout << i << ' ';
}The header has three slots, separated by two semicolons:
| Slot | Runs | Purpose |
|---|---|---|
int i = 0 | once, before anything | initialize |
i < 4 | before each iteration | test |
++i | after each iteration | advance |
The order is: initialize, test, body, advance, test, body, advance, … and the loop ends the first time the test is false.
Written as a while loop it is the same thing spread out:
int i = 0; // initialize
while (i < 4) { // test
std::cout << i << ' ';
++i; // advance
}Both compile to the same work. The for version is better for one specific reason: the advance cannot be forgotten, because it has a slot. Chapter 5.4's entire failure mode was an advance that was not written, and a reader checking a for loop sees immediately whether that slot is filled.
2. The Loop Variable Belongs to the Loop
A variable declared in the first slot exists only for the duration of the loop:
for (int i = 0; i < 3; ++i) { /* ... */ }
return i; // errorerror: use of undeclared identifier 'i'That is the behaviour you want, for the same reason Chapter 5.1 §6 wanted the if-with-initializer: a counter that is meaningless outside the loop should not be visible outside it. It also means you can use i again in the next loop without a thought.
When you do need the final value afterwards, declare the variable before the loop and leave the first slot empty:
int i = 0;
for (; i < n; ++i) { if (found(i)) break; }
// i now holds where the search stoppedThat is a legitimate use and it is worth noticing that it gives up the protection in §1 — the variable outlives the loop again, and so does any confusion about its value.
3. Any Slot Can Be Empty
All three slots are optional. Leaving all three out gives the form Chapter 5.4 §7 arrived at:
#include <iostream>
int main()
{
int i = 0;
for (;;) { // no condition: deliberately unbounded
if (i == 3) break;
std::cout << "i = " << i << '\n';
++i;
}
std::cout << "left the loop\n";
return 0;
}Expected output:
i = 0
i = 1
i = 2
left the loopChapter 5.4 §7 established why this is well-defined where while (i < 10) {} is not: an absent condition is carved out of the forward-progress assumption. This chapter just supplies the syntax.
An empty test slot means "always true". That is the only slot whose absence changes meaning rather than simply doing nothing.
4. What Belongs in the Header
The slots are expressions, so a great deal fits that should not go there.
Keep the header to one counter. The comma operator (Chapter 4.9 §6) lets you advance two things at once — for (int i = 0, j = n; i < j; ++i, --j) — and that is readable exactly once, for the two-ends-moving-inward pattern. Past that it is a second loop hidden in a header.
Do not put work in the test slot. The test runs before every iteration, so i < compute_limit() calls that function every time. If the limit does not change, compute it once:
const int limit = compute_limit();
for (int i = 0; i < limit; ++i) { /* ... */ }This is also clearer about intent: the const says the bound is fixed, which is a fact the reader otherwise has to establish by reading compute_limit.
Do not modify the counter in the body as well as the header. Two places changing one variable means the iteration count is no longer visible in the header, which removes the whole advantage of the form.
5. Bounds: Count Up, Stop Before
Here is the convention that makes loop arithmetic reliable. Start at zero, and stop before the count:
#include <iostream>
int main()
{
const int n = 4;
std::cout << "i < n :";
for (int i = 0; i < n; ++i) std::cout << ' ' << i;
std::cout << " (" << n << " iterations)\n";
std::cout << "i <= n:";
int count = 0;
for (int i = 0; i <= n; ++i) { std::cout << ' ' << i; ++count; }
std::cout << " (" << count << " iterations)\n";
return 0;
}Expected output:
i < n : 0 1 2 3 (4 iterations)
i <= n: 0 1 2 3 4 (5 iterations)i < n runs exactly n times. The count is in the condition, with no arithmetic to do. i <= n runs n + 1 times, which is almost never what anyone wanted — it is the classic off-by-one, and Chapter 5.7 will meet it again as a genuine bug rather than a demonstration.
The range 0 ≤ i < n is called half-open: closed at the bottom, open at the top. Three properties make it the right default:
- The length is the bound.
0tonholdsnitems. No+ 1anywhere. - Empty is natural.
n = 0gives zero iterations with no special case. - Ranges join cleanly.
0 to kandk to ncover0 to nexactly once, with no element shared and none skipped.
Start at zero, stop before the count. Nearly every off-by-one is a loop that broke one of those three properties.
6. The Loop That Counts Down and Never Stops
Now the measurement this chapter exists for.
Counting down looks like a mirror image of counting up, and with the wrong type for the index it is not:
#include <iostream>
int main()
{
const unsigned int n = 3;
int guard = 0;
for (unsigned int i = n - 1; i >= 0; --i) {
std::cout << "i = " << i << '\n';
if (++guard == 5) {
std::cout << "...stopped by a guard; the condition never went false\n";
break;
}
}
return 0;
}Expected output:
i = 2
i = 1
i = 0
i = 4294967295
i = 4294967294
...stopped by a guard; the condition never went falseThe first three lines are right. Then i is 0, the body runs, and --i decrements it.
An unsigned int cannot hold −1. Chapter 3.3 §4 established what happens instead: the result is taken modulo 2³², so 0 − 1 becomes 4294967295. And i >= 0 is true for every possible value of an unsigned type — that is what unsigned means. The condition cannot ever be false, so the loop cannot ever end.
Without the guard, this loop runs forever. It is not the undefined kind from Chapter 5.4, because the body does observable work — it prints. It is the straightforward kind: a loop that genuinely never stops.
The same loop with a signed index is correct:
for (int i = n - 1; i >= 0; --i) std::cout << "i = " << i << '\n';i = 2
i = 1
i = 0int can hold −1, so after i is 0 the decrement gives −1, i >= 0 is false, and the loop ends.
The compiler said nothing. Zero warnings at -Wall -Wextra -Wpedantic for the unsigned version. A warning does exist, and it is not in that set:
$ c++ -std=c++17 -Wall -Wextra -Wpedantic -Wtautological-unsigned-zero-compare countdown.cpp -o countdown
countdown.cpp:8:36: warning: result of comparison of unsigned expression >= 0 is always true [-Wtautological-unsigned-zero-compare]7. Choosing Between for and while
Both forms can express any loop. The honest split:
| Use | When |
|---|---|
for | the iteration count or the stepping pattern is known up front |
while | the loop ends on a condition discovered inside the body |
for (;;) | deliberately unbounded, exiting with break or return |
The distinction is whether the advance belongs in the header. If it is a regular step, put it there and let the reader see it. If progress happens as a side effect of the body's work — reading until end of input, retrying until success — a while loop says that more honestly than a for with an empty advance slot.
8. The Loop You Will Usually Want Instead
One thing worth saying before the next chapter, because it changes what you write.
Every loop in this chapter counts. Counting is the right tool when the number itself matters — a fixed number of retries, a step of two, a countdown. But the most common reason to write a loop is to visit each item in a collection, and for that, counting is a means to an end: you build an index only so you can use it to reach an element.
That indirection is where a whole family of bugs lives. The index can start in the wrong place, stop in the wrong place, or be used to reach past the end — and Chapter 9.1 will establish that C++ does not check the last one for you.
Chapter 5.6 is the loop that removes the index entirely. It is the loop you will write most often, and the reason this module teaches it here rather than alongside the containers it is usually demonstrated on.
9. What to Actually Do
Count up and stop before the bound. for (int i = 0; i < n; ++i) runs exactly n times with no arithmetic.
Use a signed index. Always, when the loop counts down or when the index could go below zero.
Let the loop variable belong to the loop. Declare it in the first slot unless you genuinely need the final value.
Keep one counter in the header, and keep work out of the test slot.
Hoist a fixed bound into a const before the loop.
Write a deliberately unbounded loop as for (;;) with an explicit exit — Chapter 5.4 §7.
Add -Wtautological-unsigned-zero-compare if you use unsigned indices, since -Wall -Wextra -Wpedantic does not catch §6.
10. Mistakes Worth Making Once
Writing i <= n. Measured: n + 1 iterations where you wanted n.
Counting down with an unsigned index. Measured: 2, 1, 0, 4294967295, and never stops.
Expecting a warning for that. Zero at -Wall -Wextra -Wpedantic; it needs -Wtautological-unsigned-zero-compare.
Thinking i >= 0 is ever false for an unsigned type. It is not, for any value.
Calling a function in the test slot. It runs before every iteration.
Modifying the counter in the body as well as the header. The iteration count is no longer where the reader looks for it.
Declaring the counter before the loop out of habit. It then outlives the loop, and so does any uncertainty about its value.
Using the comma operator for more than the two-ends-inward pattern. Past that it is a hidden second loop.
11. What You Should Be Able to Say Now
- A
forheader has three slots — initialize, test, advance — and the advance cannot be forgotten because it has a place to be. - The order is initialize, then test / body / advance repeatedly, ending the first time the test is false.
- A variable declared in the first slot is scoped to the loop; using it afterwards is a compile error.
- Any slot may be empty, and an empty test means "always true" — which is Chapter 5.4 §7's well-defined
for (;;). i < nruns exactlyntimes;i <= nrunsn + 1. Measured: 4 against 5.- The half-open range
0 ≤ i < nmakes the length equal the bound, makes the empty case natural, and lets ranges join without overlap. - Measured:
for (unsigned int i = n - 1; i >= 0; --i)printed 2, 1, 0, 4294967295, and could not terminate. i >= 0is true for every value of an unsigned type, so the condition can never be false.- The same loop with an
intindex prints 2, 1, 0 and stops. - Zero warnings at
-Wall -Wextra -Wpedantic;-Wtautological-unsigned-zero-comparereports it. - That is the fourth appearance of the modulo-2ᴺ rule — 3.3 §4, 4.4 §3, 5.2 §8, and now nontermination.
- Use a signed type for anything that counts down. When forced to index with unsigned,
for (unsigned i = n; i-- > 0;)is the correct idiom. - Use
forwhen the stepping pattern is known up front,whilewhen the exit is discovered in the body.
12. Exercises
Exercise 1 — count the iterations. For n = 5, give the number of body executions and the first and last values of i: (a) i = 0; i < n; ++i (b) i = 0; i <= n; ++i (c) i = 1; i < n; ++i (d) i = 0; i < n; i += 2.
Exercise 2 — pick the bound. Write the header for each: (a) exactly 8 iterations starting at 0 (b) the values 1 through 10 (c) the even numbers 0 through 14 (d) 3 iterations counting down from 2 to 0.
Exercise 3 — trace the countdown. For §6's unsigned loop with n = 1, list the values printed before the guard fires, and say which decrement produces the first wrong one.
Exercise 4 — fix it three ways. Repair §6's loop so it prints 2, 1, 0 and stops: (a) by changing the index type, (b) with the i-- > 0 idiom, (c) by counting upward and computing the downward value in the body. Say which you would ship.
Exercise 5 — the missing flag. State the exact warning that catches §6, say whether -Wall -Wextra -Wpedantic includes it, and name the other chapter in this module whose headline bug needs an extra flag too.
Exercise 6 — for or while? For each, choose and justify: (a) send exactly 16 bytes (b) read until end of input (c) retry up to three times, stopping early on success (d) halve a value until it reaches 1 (e) run forever until a shutdown flag is set.
Exercise 7 — three properties. The half-open range has three stated properties. For each, give a concrete loop over n = 0 or a pair of adjacent ranges that shows what breaks if you use 0 ≤ i ≤ n instead.
13. Summary
- A
forheader is initialize, test, advance — the advance has a slot, so Chapter 5.4's failure mode largely disappears. - The loop variable is scoped to the loop; using it afterwards is a compile error.
- Any slot may be empty; an empty test means always true, which is the well-defined
for (;;). i < nrunsntimes;i <= nrunsn + 1. Measured: 4 against 5.- Half-open ranges make the length equal the bound, handle empty naturally, and join cleanly.
- Measured: an
unsignedcountdown printed 2, 1, 0, 4294967295 and never terminated, with zero warnings. i >= 0is true for every unsigned value;-Wtautological-unsigned-zero-compareis the flag.- That is the fourth appearance of modulo-2ᴺ wrapping, and the first where the cost is nontermination.
- Count down with a signed type, or use
for (unsigned i = n; i-- > 0;). forwhen the stepping is known up front;whilewhen the body discovers the exit.
Every loop in this chapter built a number so that something else could be reached with it. The next chapter removes the number. It is the loop you will write most often in real C++, it eliminates both ends of the bounds problem at once, and it introduces a decision this chapter never had to make — because once the loop hands you the element itself rather than its position, you have to say whether you want the element, a copy of it, or permission to change it. Getting that choice wrong silently throws away your work, and the compiler catches it in only one narrow case.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
