C++ Part I · Module 5 · Control Flow
while and do-while Loops
Every construct in this module so far has made a decision once and moved on. A loop runs the same statements repeatedly, and that introduces a requirement none of the earlier chapters had to meet: a decision either happens or it does not, but a loop has to stop. Half of this chapter is therefore familiar — the condition is an expression, exactly as Chapter 5.1 established — and the other half is the part nobody teaches carefully. Making progress toward the condition becoming false is not a style guideline. For a loop that does no observable work, the standard permits the compiler to assume termination outright, which means a loop that cannot terminate is not merely slow; it is undefined, and the compiler may do anything at all with the code around it. That is usually stated and rarely demonstrated, so this chapter demonstrates it: one short program, compiled at two optimisation levels, behaved two entirely different ways, and the faster build did not hang. It also did not print what the source says it prints. The chapter closes on the two forms of infinite loop that are genuinely safe, and why they are different.
Foundation16 min readC++Part IFundamentalsControl Flow
Part I · Module 5 · Chapter 5.4 · while and do-while Loops
1. A Loop Is a Condition Plus a Guarantee
The while loop is the simplest form of repetition in C++:
while (remaining > 0) {
send_one();
--remaining;
}Read it as: while the condition is true, run the body, then test again.
Half of that is Chapter 5.1 material. remaining > 0 is an expression producing a bool, and the loop evaluates it the same way an if does. Nothing new.
The new part is the second line of the body. --remaining is what makes the condition eventually false, and without it the loop never ends. That is not an incidental detail of this example — it is a requirement, and it does not come from the language's syntax. Nothing stops you compiling a loop that cannot terminate.
A loop is two things: a condition, and a guarantee that the body moves the program toward making that condition false. The language checks the first and not the second. §5 is about what happens when the second is missing, and the answer is worse than you expect.
2. while: Test Before the Body
The condition is evaluated first, so a while loop can run its body zero times:
int remaining = 0;
while (remaining > 0) { /* never runs */ }That is the normal and usually correct behaviour. A loop that processes items should do nothing when there are no items.
3. do-while: Test After the Body
Sometimes you want the body to run at least once — most often when the body is what produces the value the condition tests. do-while moves the test to the end:
do {
command = read_command();
} while (command != 'q');The body runs, then the condition is checked. You cannot test command before reading it, so a while loop here would have to either duplicate the read before the loop or initialize command to a lie.
Measured side by side, with remaining starting at zero:
#include <iostream>
int main()
{
int remaining = 0;
int while_body = 0;
while (remaining > 0) { ++while_body; --remaining; }
int do_body = 0;
do { ++do_body; --remaining; } while (remaining > 0);
std::cout << "while body ran " << while_body << " time(s)\n";
std::cout << "do-while body ran " << do_body << " time(s)\n";
return 0;
}Expected output:
while body ran 0 time(s)
do-while body ran 1 time(s)That one-iteration difference is the entire distinction between the two forms. Everything else about them is identical.
do-while needs a trailing semicolon. The while (...) at the end is part of the statement, not the start of a new one:
error: expected ';' after do/while statementUse do-while rarely. It is the right tool when the body must run before the condition can be meaningful, and it is harder to read than a while because the condition is at the bottom, after the code it governs. When both forms work, prefer while.
4. The Three Questions to Ask of Any Loop
Before writing the body, answer these. They catch most loop bugs before they exist.
- What makes the condition false? Name the statement. If you cannot, the loop does not terminate.
- Does that statement always run? A
continueor an early branch that skips it reintroduces the problem — Chapter 5.7 measures exactly that. - Is the first test correct? Should the body run zero times when the condition starts false? If yes,
while. If it must run once regardless,do-while.
5. What Happens When There Is No Progress
Here is the program. It prints, loops with no way out, and prints again:
#include <iostream>
int main()
{
std::cout << "before" << std::endl; // endl flushes
int i = 0;
while (i < 10) { /* no progress, no side effects */ }
std::cout << "after" << std::endl;
return 0;
}i is never modified, so i < 10 is true forever. Read as ordinary code, this program prints before and then hangs, and after is unreachable.
Compiled with no optimisation, that is what happens:
$ c++ -std=c++17 -Wall -Wextra -Wpedantic noprogress.cpp -o noprogress
$ ← zero warnings
$ ./noprogress
before
^C ← hangs; killed after 3 secondsCompiled with optimisation, the same source does something else:
$ c++ -std=c++17 -O2 -Wall -Wextra -Wpedantic noprogress.cpp -o noprogress
$ ← zero warnings
$ ./noprogress
before
$ echo $?
133It did not hang. It died. Exit code 133 is 128 + 5, which is SIGTRAP — the program hit a trap instruction.
And it did not print after either. Looking at the generated code shows why: at -O2 the loop is gone and a single trap instruction stands where it was. Counting instructions in the two builds:
| build | trap instructions in main | backward branches in main |
|---|---|---|
-O2, no side effects | 1 | 0 |
-O2, with an observable side effect | 0 | 1 |
The second row is the control. Change the body to do something observable and the loop survives optimisation intact — it loops forever at -O2, exactly as written, with no trap anywhere. So this is not the optimiser being aggressive in general. It is the optimiser acting on a specific permission.
6. The Permission: Forward Progress
The standard says that a loop which does no observable work — no input or output, no volatile access, no synchronisation — and whose condition is not a constant expression may be assumed by the implementation to terminate.
i < 10 is not a constant expression; i is a variable. So the compiler is entitled to assume this loop finishes. It cannot finish. The assumption is false, and a program that breaks an assumption the standard grants is undefined — the compiler owes it nothing at all, and what clang chose to emit was a trap.
Two consequences, both measured, both worth carrying.
Undefined behaviour is not confined to the undefined part. before was printed by a statement that ran correctly before the loop was ever reached — and it did not appear in the output of the first version of this experiment, which used '\n' instead of std::endl. The characters were sitting in std::cout's buffer when the trap killed the process, and a killed process does not flush. Chapter 2.8 §5 measured exactly this — a program that wrote a line, died, and left an empty file behind. Correct output from a correct statement was lost because of undefined behaviour later in the program, which is Chapter 1.3 §5's definition doing its work: the standard places no requirements on the program — not on the part that was wrong, on the program.
Zero warnings, at every optimisation level. The compiler detected this well enough to replace the loop with a trap and did not mention it. It is not obliged to, and in general it cannot prove a loop fails to terminate — here it did not need to prove anything, because it was allowed to assume the opposite.
7. The Two Infinite Loops That Are Safe
A deliberate loop with no condition is not undefined, and the measurement shows the difference plainly. Three loops, all with empty bodies, all compiled at -O2:
| loop | condition | at -O2 |
|---|---|---|
while (i < 10) {} | not a constant expression | trap — SIGTRAP, exit 133 |
for (;;) {} | none at all | loops forever |
while (true) {} | a constant expression | loops forever |
The bottom two rows are the carve-out. A loop whose condition is absent or is a constant expression is exempt from the forward-progress assumption, so it is well-defined, and clang left both alone — no trap, and the backward branch still in the generated code.
That makes for (;;) with an explicit exit the correct way to write an intentionally unbounded loop:
#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, at -O2:
i = 0
i = 1
i = 2
left the loopfor is Chapter 5.5's subject and break is Chapter 5.7's; both appear here because this is the shape the carve-out licenses, and splitting it across three chapters would leave §5 without its answer.
The reading of for (;;) is worth stating directly: it is not a loop with a missing condition, it is a loop whose exit is inside the body. A reader seeing for (;;) knows to look for a break or a return. A reader seeing while (i < 10) knows to look for something that changes i — and §5 is what happens when they do not find it.
8. What to Actually Do
Name the statement that makes the condition false before writing the body. If there is not one, you are not finished.
Check that it always runs — not just on the path you were thinking about.
Prefer while. Use do-while only when the body must run before the condition can be evaluated.
Write a deliberate unbounded loop as for (;;) with a break or return, not as a condition you know will never be false.
Do not use while (i < 10) {} as a pause or a spin. It is undefined, and the measured consequence was a trap.
Treat a hang as a bug with two possible symptoms. The same defect may hang one build and crash another.
9. Mistakes Worth Making Once
Writing a loop with no progress statement. Measured: hangs at -O0, SIGTRAP at -O2, zero warnings at both.
Expecting an infinite loop to simply hang. It is undefined when the condition is not a constant expression, and the compiler may do anything.
Expecting the statements before the loop to be safe. Measured: the buffered before was lost when the trap killed the process.
Expecting a warning. None appeared at any optimisation level.
Forgetting the semicolon after do-while. error: expected ';' after do/while statement.
Using do-while by default. It runs the body before anything has been checked; while is the safer shape.
Assuming while (true) is the same as while (flag) with flag always true. The first is a constant expression and exempt; the second is not and is undefined.
10. Interview Relevance
"What happens if a loop never terminates?" has a weak answer and a strong one.
The weak answer is "the program hangs". The strong answer distinguishes the two cases: if the loop does observable work, or its condition is a constant expression, it is well-defined and it runs forever; if it does no observable work and its condition is not a constant expression, the behaviour is undefined, because the standard lets the implementation assume termination.
If you can add what that looks like in practice — a trap instruction instead of a loop, and output from before the loop lost to an unflushed buffer — you are describing something you have actually run. That is the difference the question is looking for.
11. What You Should Be Able to Say Now
- A loop is a condition plus a guarantee of progress. The language checks the condition and not the guarantee.
whiletests before the body, so the body can run zero times.do-whiletests after, so the body runs at least once. Measured: 0 against 1 withremaining = 0.do-whileneeds a trailing semicolon —error: expected ';' after do/while statement.- Ask three questions of every loop: what makes the condition false, does it always run, and is the first test right.
- A loop doing no observable work whose condition is not a constant expression may be assumed to terminate — so one that cannot is undefined.
- Measured:
while (i < 10) {}hung at-O0and died with SIGTRAP, exit 133 at-O2. - At
-O2the loop was replaced by one trap instruction; the same shape with an observable side effect kept its backward branch and looped forever. - Zero warnings at every optimisation level.
- The buffered
beforewas lost — undefined behaviour later in the program destroyed correct output from earlier in it. - The rule exists so compilers can move code across loops; it is not a ban on infinite loops.
for (;;)andwhile (true)are carved out — no condition, or a constant one — so both are well-defined, and clang left both looping.- Write a deliberate unbounded loop as
for (;;)with an explicitbreakorreturn.
12. Exercises
Exercise 1 — count the iterations. For remaining = 0, 1 and 3, give the number of body executions for while (remaining > 0) { --remaining; } and for the do-while equivalent.
Exercise 2 — choose the form. For each, say while or do-while and why: (a) drain a queue that may be empty (b) read commands until the user types q (c) retry a transfer up to three times (d) prompt for a value until it is in range.
Exercise 3 — name the progress. For each, name the statement that makes the condition false, or say that there is not one: (a) while (n > 0) { n -= 2; } (b) while (n != 1) { n -= 2; } with n starting at 10 (c) while (c != 'q') { c = read(); } (d) while (busy) { }.
Exercise 4 — explain two behaviours. The same source hung at -O0 and exited with code 133 at -O2. Explain what permission the compiler was using, and why that permission does not apply to a loop that prints.
Exercise 5 — the missing output. The program's first statement printed before successfully, yet before was absent from the output of one build. Explain both why it was missing and what one change makes it appear.
Exercise 6 — classify the three loops. For while (i < 10) {}, while (true) {} and for (;;) {} with empty bodies: say which are undefined and which are well-defined, and state the single property that separates them.
Exercise 7 — write it safely. A bare-metal program must never return from main. Write the loop, say why your version is well-defined, and say what you would write instead if the loop should exit on a flag.
13. Summary
- A loop is a condition plus a guarantee of progress — only the first is checked for you.
whiletests first and can run zero times;do-whiletests last and runs at least once. Measured: 0 against 1.do-whileneeds its trailing semicolon.- A loop doing no observable work whose condition is not a constant expression may be assumed to terminate, so one that cannot is undefined.
- Measured: hung at
-O0; SIGTRAP, exit 133 at-O2, where the loop became one trap instruction. - With an observable side effect the loop survived
-O2and ran forever — the trap was licensed by UB, not by optimisation. - Zero warnings anywhere, and the buffered output from before the loop was lost.
for (;;)andwhile (true)are carved out and remain well-defined.- Write deliberate unbounded loops as
for (;;)with an explicit exit. - Ask what makes the condition false, and whether it always runs.
The next chapter takes the same three parts — initialize, test, advance — and puts them on one line where you can see all three at once. That makes the progress statement impossible to forget, which removes this chapter's failure mode almost entirely, and replaces it with a different one: the loop now runs a specific number of times, and getting that number right at both ends is its own skill. One of the measurements there is a loop that counts downward and never stops, written by someone who chose the wrong type for the index.
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.
