C++ / CONTROL FLOW
while loops and their termination guarantees
Trace a while loop's iteration count, prove it terminates with a bounded decreasing measure, and spot guards that silently spin forever.
What you will learn
- Predict how many times a while body runs, including the zero-iteration case
- Name the bounded, decreasing quantity that proves a while loop terminates
- Spot unsigned countdowns and never-updated guards that loop forever
- Explain why a side-effect-free infinite loop is undefined behaviour in C++
Understanding while loops and their termination guarantees
A while loop evaluates its condition before every pass, including the first, so a loop whose test is already false runs its body zero times. The condition is re-evaluated fresh each pass rather than captured once, so it reads whatever the named variables currently hold. Nothing in the language links the condition to the body: while (n > 1) will spin happily if the body never writes to n, and the compiler will not complain.
Termination is your proof obligation. The standard argument is a loop variant: pick a quantity that appears in the condition, show that every pass moves it strictly in one direction, and show that direction runs into a bound at which the test fails. Halving a positive int reaches 1, consuming characters from a stream exhausts it, and --i on a signed int eventually drops below zero. When you cannot name such a measure you do not have a terminating loop: while (x != 1.0) x += 0.1; never stops, because repeatedly adding a binary approximation of 0.1 never lands exactly on 1.0.
C++ goes further than merely not helping: an implementation is allowed to assume that every thread eventually terminates, performs I/O, touches a volatile object, or does an atomic or synchronisation operation. A loop that spins forever while doing none of those is undefined behaviour, not just a hang, so an optimiser may delete it, fall through it, or hoist later code above it, and the resulting misbehaviour can appear far from the loop. That is why a deliberate endless loop should do observable work on each pass, or be written as while (true) with a break; C++26 additionally carves out trivial infinite loops such as while (true) {} as well-defined.
<iostream>
int main() {
int n = 100;
int steps = 0;
// The guard is tested before every pass, so the body must move n toward 1.
while (n > 1) {
n /= 2; // n strictly decreases and 1 is a hard floor
++steps;
std::cout << "pass " << steps << ": n = " << n << '\n';
}
std::cout << "finished after " << steps << " passes\n";
int pending = 0;
while (pending > 0) { // false on entry, so the body never runs at all
std::cout << "unreachable\n";
--pending;
}
std::cout << "second loop ran 0 times\n";
}
A while loop stops only because its body drives some bounded quantity in the condition toward the point where the test fails, and the language assumes you arranged that rather than checking it.
Worked examples
Counting down with an unsigned variable
Shows why an unsigned counter must be guarded with > 0 and never with >= 0.
<iostream>
int main() {
std::cout << std::boolalpha;
unsigned int i = 3;
std::cout << "countdown:";
while (i > 0) { // > 0, not >= 0
std::cout << ' ' << i;
--i;
}
std::cout << '\n';
std::cout << "i is 0 now, and (i >= 0) is " << (i >= 0) << '\n';
--i; // unsigned arithmetic wraps instead of going negative
std::cout << "after one more --i, (i > 0) is " << (i > 0) << '\n';
}
Example explained
Line 1while (i > 0) fails exactly when i reaches 0, so the body runs three times and the measure has a reachable bound.
Line 2(i >= 0) is true for every value an unsigned int can hold, so it can never serve as an exit test; most compilers warn that the comparison is always true.
Line 3--i at 0 wraps to the largest unsigned int value, a well-defined modular result, so the countdown would restart rather than end.
Line 4With int instead of unsigned int, --i would reach -1 and i >= 0 would terminate: the type of the counter, not the shape of the loop, decides.
A guard that consumes what it tests
Uses stream extraction as the condition so that each pass makes measurable progress through the input.
<iostream>
<sstream>
int main() {
std::istringstream in("12 7 x 5");
int value = 0;
int sum = 0;
while (in >> value) { // the test itself advances the stream
sum += value;
}
std::cout << "sum = " << sum << '\n';
std::cout << std::boolalpha
<< "eof = " << in.eof() << ", fail = " << in.fail() << '\n';
}
Example explained
Line 1The condition both consumes characters and reports whether to continue, so the decreasing measure is the unread part of the input.
Line 2Extraction stops at x without reading it, which sets failbit while leaving eofbit clear, so the loop ends after adding 12 and 7.
Line 3Writing while (!in.eof()) here would never terminate: the failed extraction leaves eof() false and the body changes nothing, so the condition is stuck at true.
Line 4The trailing 5 is never read, which is the correct behaviour for a loop whose exit condition is the first failure.
Important notes
A stray semicolon turns while (i < 10); into a loop with an empty body: the guard can never change, and because the loop has no side effects the behaviour is undefined rather than a predictable hang.
An accidental endless loop that does no I/O, no volatile access, and no atomic operation is undefined behaviour, so the symptom may be corrupted control flow elsewhere instead of a program that simply sits there.
Common mistakes
Testing a variable the body never assigns, such as while (n > 1) with a body that computes n / 2 but discards the result: the condition stays true and the program hangs with no diagnostic.
Counting down an unsigned variable with while (i >= 0): after i reaches 0, --i wraps to the type's maximum, the test never fails, and the loop never ends.
Guarding a floating-point accumulation with equality, as in while (x != 1.0) x += 0.1;: the sum steps past 1.0 without ever equalling it, so the loop runs forever even though the arithmetic looks exact on paper.
Try it yourself
Change, predict, then run
Write a while loop that counts the decimal digits of a non-negative int by repeatedly dividing by 10, run it with 0 and notice it reports 0 digits, then fix it so 0 reports 1 while keeping the division as the decreasing measure.
Open the C++ workspaceCheck your understanding
An unsigned int n holds a value you do not control. Which loop is guaranteed to stop for every possible starting value?
- while (n != 0) { n /= 2; }
- while (n != 1) { n /= 2; }
- while (n >= 0) { --n; }
- while (n % 2 == 0) { n += 2; }
Show answer
Halving a nonzero unsigned value strictly decreases it and 1 / 2 is 0, so n reaches 0 from any start and the test fails. while (n != 1) looks like the same loop, but 0 is a fixed point of halving: a starting value of 0 stays 0 and never becomes 1. while (n >= 0) can never be false for an unsigned type, and n += 2 on an even value wraps modulo the type's range while staying even forever.