C++ / CONTROL FLOW
do-while loops and single-execution cases
Use do-while when the first pass must happen before the test, spot the cases where it's wrong, and read the do { } while (false) single-run idiom.
What you will learn
- Reach for do-while when the test depends on work the first pass performs.
- Know that do-while cannot run zero times, so empty input still executes the body.
- Declare any state the condition reads before the do, since the body's scope ends first.
- Use do { ... } while (false) with break to give a block one exit point.
Understanding do-while loops and single-execution cases
A do statement puts the test at the bottom: control enters the body unconditionally, then the condition is evaluated and jumps back to the top while it is true. That one structural difference from while is the whole story behind the at-least-once guarantee, because there is no edge into the loop that can skip the body. The grammar also ends the statement with a semicolon after while (expression), so forgetting it is a syntax error rather than a style problem.
The shape fits when the condition needs information that only the first pass can produce. Turning a number into digits is the canonical case: you cannot know whether more digits remain until you have divided once, and zero has one digit even though the continuation test fails immediately. Written as a while loop you must either duplicate the first pass above the loop or invent a seed value, and the duplicate drifts as the body changes. The mirror image is the trap: when doing nothing is a legal outcome, such as an empty container or an already-satisfied condition, do-while performs the work once anyway.
Writing while (false) makes the body run exactly once, which is useful precisely because break then becomes a structured jump to the end of the block. That gives a sequence of early-rejection checks one exit point with shared trailing code after the loop, and it is why function-like macros are wrapped this way: the expansion is a single statement that still consumes the caller's semicolon. The constant condition costs nothing at runtime, but prefer a small function with early returns when returning is an option; the idiom earns its place where you cannot return.
<iostream>
<string>
std::string digitsWhile(unsigned n) {
std::string s;
while (n > 0) {
s.insert(s.begin(), char('0' + n % 10));
n /= 10;
}
return s;
}
std::string digitsDo(unsigned n) {
std::string s;
do {
s.insert(s.begin(), char('0' + n % 10));
n /= 10;
} while (n > 0);
return s;
}
int main() {
for (unsigned n : {0u, 7u, 4096u}) {
std::cout << n << ": while=[" << digitsWhile(n)
<< "] do=[" << digitsDo(n) << "]\n";
}
}do-while tests at the bottom, so the body always runs at least once and the condition can only see state declared outside it.
Worked examples
One exit point with while (false)
A block that runs exactly once, using break to leave early while shared trailing code still runs.
<iostream>
<string>
void process(const std::string& name) {
do {
if (name.empty()) { std::cout << "reject: empty\n"; break; }
if (name.size() > 8) { std::cout << "reject: too long\n"; break; }
if (name.front() == '_') { std::cout << "reject: leading underscore\n"; break; }
std::cout << "accept: " << name << '\n';
} while (false);
std::cout << " done with " << name.size() << " chars\n";
}
int main() {
process("");
process("_hidden");
process("configuration");
process("width");
}Example explained
Line 1while (false) never loops back, so the block is a single-pass region and each break means "skip the remaining checks".
Line 2The empty check must come first: name.front() on an empty string is undefined behaviour, and breaking early is what keeps that line unreachable.
Line 3The "done with" line lives after the loop, so every path including the rejections reaches it exactly once.
Line 4A helper function with returns would read the same; this form is for when the trailing lines must stay in the current scope.
Splitting where one field always exists
Shows a loop whose continuation test can only be answered after the current field has been produced.
<iostream>
<string>
void split(const std::string& version) {
std::size_t start = 0, dot = 0;
int count = 0;
do {
dot = version.find('.', start);
std::cout << " [" << version.substr(start, dot - start) << "]\n";
++count;
start = dot + 1;
} while (dot != std::string::npos);
std::cout << version << " -> " << count << " field(s)\n";
}
int main() {
split("3.14.0");
split("7");
}Example explained
Line 1find returns npos on the last field, so the loop only learns it is finished after emitting that field.
Line 2substr(start, dot - start) with dot == npos asks for an enormous count, and substr clamps it to the end of the string, so the final field arrives intact.
Line 3split("7") demonstrates the guarantee: no separator at all still yields one field, while a top-tested loop on dot != npos would print nothing.
Line 4start = dot + 1 wraps to 0 on the last pass, which is defined unsigned arithmetic and harmless because the condition ends the loop before start is read again.
Sentinel loop with a hoisted variable
Demonstrates why the variable the condition tests has to be declared above the do.
<iostream>
<vector>
int main() {
std::vector<char> fakeInput{'x', 'a', 'q'};
std::size_t i = 0;
char choice = '?';
do {
choice = (i < fakeInput.size()) ? fakeInput[i++] : 'q';
std::cout << "menu> " << choice << '\n';
if (choice == 'a') std::cout << " adding an item\n";
else if (choice != 'q') std::cout << " unknown command\n";
} while (choice != 'q');
std::cout << "exited after " << i << " command(s)\n";
}Example explained
Line 1choice is declared before the do because the condition at the bottom reads it; a declaration inside the braces would not be visible there.
Line 2The body consumes and echoes one command before any test, which is the shape you want for a prompt that must appear at least once.
Line 3The sentinel 'q' is handled by the body first and only then stops the loop, which is why the final menu> q line appears.
Line 4i ends at 3 because the sentinel itself counts as a consumed command.
Important notes
There is no do-while form that declares in the condition; while (int c = get()) is legal, but a do-while condition is a plain expression, so state must exist before the loop.
continue inside a do-while jumps to the condition test, not to the top of the body, so the test still runs before the next pass begins.
Common mistakes
Dropping the semicolon after while (condition): the compiler reports an unexpected token on the following line, so beginners edit the wrong line looking for the error.
Walking a container with do { total += v[i]; ++i; } while (i < v.size());, which reads v[0] on an empty container because the bounds test happens after the access, giving undefined behaviour that survives testing on non-empty data.
Declaring the loop variable inside the body and again outside to satisfy the condition: the inner declaration shadows the outer one, the condition keeps testing the untouched outer value, and the program hangs.
Try it yourself
Change, predict, then run
Write a do-while that prints the binary digits of an unsigned value from least significant to most, then run it with 0 and with 6. Rewrite the same loop as a while and confirm the 0 case prints nothing.
Open the C++ workspaceCheck your understanding
A function sums a vector with std::size_t i = 0; do { total += v[i]; ++i; } while (i < v.size()); It passes every test but crashes on some production inputs. What is the flaw?
- The condition should be i <= v.size() so the final element is not skipped.
- i must be declared inside the do block, otherwise the condition reads a stale value.
- An empty vector still enters the body once, so v[0] is read out of bounds.
- ++i runs after the condition, so element 0 is added twice.
Show answer
Because the test sits at the bottom, the first element is read before any bounds check, so a vector of size 0 accesses past the end and the behaviour is undefined. The i <= v.size() option is tempting because it looks like an off-by-one repair, but it would read one past the end for every input and does nothing about the empty case.