Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    for (int i = 0; i < 4; ++i) {
        std::cout << i << ' ';
    }

The header has three slots, separated by two semicolons:

SlotRunsPurpose
int i = 0once, before anythinginitialize
i < 4before each iterationtest
++iafter each iterationadvance

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    for (int i = 0; i < 3; ++i) { /* ... */ }
    return i;                        // error
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int i = 0;
    for (; i < n; ++i) { if (found(i)) break; }
    // i now holds where the search stopped

That 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:

Azvya Education Pvt. Ltd.VLSI Mentor
deliberate.cpp — complete program
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
i = 0
i = 1
i = 2
left the loop

Chapter 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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:

Azvya Education Pvt. Ltd.VLSI Mentor
bounds.cpp — complete program
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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. 0 to n holds n items. No + 1 anywhere.
  • Empty is natural. n = 0 gives zero iterations with no special case.
  • Ranges join cleanly. 0 to k and k to n cover 0 to n exactly 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:

Azvya Education Pvt. Ltd.VLSI Mentor
countdown.cpp — complete program. It cannot terminate; the guard is the only exit.
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
i = 2
i = 1
i = 0
i = 4294967295
i = 4294967294
...stopped by a guard; the condition never went false

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    for (int i = n - 1; i >= 0; --i) std::cout << "i = " << i << '\n';
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
i = 2
i = 1
i = 0

int 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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:

UseWhen
forthe iteration count or the stepping pattern is known up front
whilethe 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

  1. A for header has three slots — initialize, test, advance — and the advance cannot be forgotten because it has a place to be.
  2. The order is initialize, then test / body / advance repeatedly, ending the first time the test is false.
  3. A variable declared in the first slot is scoped to the loop; using it afterwards is a compile error.
  4. Any slot may be empty, and an empty test means "always true" — which is Chapter 5.4 §7's well-defined for (;;).
  5. i < n runs exactly n times; i <= n runs n + 1. Measured: 4 against 5.
  6. The half-open range 0 ≤ i < n makes the length equal the bound, makes the empty case natural, and lets ranges join without overlap.
  7. Measured: for (unsigned int i = n - 1; i >= 0; --i) printed 2, 1, 0, 4294967295, and could not terminate.
  8. i >= 0 is true for every value of an unsigned type, so the condition can never be false.
  9. The same loop with an int index prints 2, 1, 0 and stops.
  10. Zero warnings at -Wall -Wextra -Wpedantic; -Wtautological-unsigned-zero-compare reports it.
  11. That is the fourth appearance of the modulo-2ᴺ rule — 3.3 §4, 4.4 §3, 5.2 §8, and now nontermination.
  12. 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.
  13. Use for when the stepping pattern is known up front, while when 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 for header 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 < n runs n times; i <= n runs n + 1. Measured: 4 against 5.
  • Half-open ranges make the length equal the bound, handle empty naturally, and join cleanly.
  • Measured: an unsigned countdown printed 2, 1, 0, 4294967295 and never terminated, with zero warnings.
  • i >= 0 is true for every unsigned value; -Wtautological-unsigned-zero-compare is 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;).
  • for when the stepping is known up front; while when 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.