C++ Part I · Module 5 · Control Flow
if, else and Multi-Way Decisions
Every program in this curriculum so far has run straight through from its first statement to its last. That is enough to demonstrate a type or an operator, and it is not enough to do anything useful, because useful programs behave differently depending on what they find. This chapter adds the statement that chooses. Almost nothing in it is new: a condition is an expression, which Chapter 4.1 defined; it produces a bool, which Chapter 4.4 covered; and the operators that build it short-circuit in a way Chapter 4.5 made into a guarantee. The genuinely new idea is small — a statement can select between two others — and the chapter spends its length on the three things that make it go wrong anyway: the order of a multi-way chain, which is part of its meaning rather than a matter of taste; an else that attaches to a different if than the indentation suggests; and a condition that quietly assigns instead of comparing. The compiler warns about two of those three, and the chapter says which.
Foundation15 min readC++Part IFundamentalsControl Flow
Part I · Module 5 · Chapter 5.1 · if, else and Multi-Way Decisions
1. A Condition Is Just an Expression
Module 4 was about expressions. This module is about statements that use an expression's value to decide what runs next.
That is the whole idea, and it means you already know most of it:
if (cycles > limit) {
std::cout << "over budget\n";
}cycles > limit is an expression. Chapter 4.4 established that a comparison produces a bool. The if statement evaluates that expression and runs the block only when the result is true.
So there is no new machinery here — no new type, no new operator. There is one new kind of statement, and it is the first one in this curriculum whose effect is to not run some code.
A condition is an expression whose value selects what runs next. Everything in this module follows from that sentence.
2. if and else
The basic form adds a second path:
if (cycles > limit) {
std::cout << "over budget\n";
} else {
std::cout << "within budget\n";
}Exactly one of the two blocks runs. There is no way for both to run and no way for neither to run.
The braces are optional and you should write them anyway. C++ lets a single statement follow the condition without them:
if (cycles > limit)
std::cout << "over budget\n";This is legal and it is the source of two separate bugs that §7 and Chapter 5.7 will measure. The cost of always writing braces is two characters; the cost of omitting them is a class of defect that survives code review because the code looks right.
3. The Condition Must Be Something That Converts to bool
C++ does not require the condition to be a bool — it requires that the condition's value can be converted to one. Chapter 3.6 covered which values convert to false: zero for the arithmetic types, and that is essentially it. Everything else converts to true.
So this is legal:
if (remaining) // true whenever remaining != 0and it means the same as if (remaining != 0). The second form is better, not because the first is wrong, but because the second says what is being tested. Chapter 3.6 §7 made the same argument; this is where it pays.
4. Multi-Way Decisions: the else-if Chain
To choose between more than two paths, put an if in the else:
#include <iostream>
static const char* classify(int mhz)
{
if (mhz < 100) return "slow";
else if (mhz < 1000) return "medium";
else return "fast";
}
int main()
{
for (int mhz : {50, 100, 999, 1000, 3200})
std::cout << mhz << " -> " << classify(mhz) << '\n';
return 0;
}Expected output:
50 -> slow
100 -> medium
999 -> medium
1000 -> fast
3200 -> fastTwo things in that output are worth stopping on.
First match wins. 999 satisfies mhz < 1000, so it is "medium" and the third branch is never reached. An else-if chain tests its conditions in order and stops at the first one that is true.
Therefore the order is part of the meaning. Reverse the first two conditions and 50 becomes "medium", because 50 < 1000 is also true. A chain whose conditions overlap is not wrong — it is ordered, and the order carries information. That is worth saying explicitly because it is the difference between a chain you can reorder safely and one you cannot.
Boundaries belong to exactly one branch. 100 is "medium" and 1000 is "fast" because both conditions use < rather than <=. There is no value that falls through all three branches, and no value that matches two. Checking the boundary cases — not the middle of each range — is how you find out whether a chain is right.
5. Scoping a Variable to the Branch That Needs It
A variable declared inside a branch exists only in that branch:
if (over_budget) {
int excess = cycles - limit; // exists only here
std::cout << "over by " << excess << '\n';
}
// excess does not exist hereVerified — referring to it afterwards is a compile error, not a warning:
error: use of undeclared identifier 'inner'That is Chapter 3.1's scope rule applied to a block, and it is the behaviour you want: a name that is meaningful only inside one branch should not be visible outside it.
But there is a common case the block form does not cover. Often you need a value to build the condition and then again inside the branch:
int status = read_status();
if (status != 0) {
std::cout << "error status " << status << '\n';
}
// status is still here, and nothing below needs itThat works, and it leaves status in scope for the rest of the function, where it is dead weight that a later reader has to account for. C++17 added a form that fixes it.
6. The if Statement with an Initializer
You can declare the variable inside the if itself, separated from the condition by a semicolon:
#include <iostream>
static int read_status() { return 3; }
int main()
{
if (int status = read_status(); status != 0) {
std::cout << "error status " << status << '\n';
} else {
std::cout << "clean\n";
}
// status is out of scope here
return 0;
}Expected output:
error status 3Read it as two parts: initialize, then test. status is created, the condition is evaluated, and status lives until the end of the whole if statement — which means it is available in the else branch too, and gone afterwards.
This is a C++17 addition. Compiling the same program against an earlier standard says so:
$ c++ -std=c++14 -Wall -Wextra -Wpedantic initform.cpp -o initform
initform.cpp:7:9: warning: 'if' initialization statements are a C++17 extension [-Wc++17-extensions]C++17 is this curriculum's baseline, so the form is available to you throughout. It is worth knowing it is recent, because you will read a great deal of code written before it existed, where the two-line version is the only option.
Chapter 7.1 will return to this as a scoping tool. For now the rule of thumb is narrow and reliable: if a variable exists only to be tested, declare it in the if.
7. Two Mistakes the Compiler Catches
Both of these are famous, both are measured here, and both produce a diagnostic at -Wall -Wextra -Wpedantic.
Assigning where you meant to compare. = is assignment (Chapter 4.6) and == is comparison (Chapter 4.4). Writing the first where you meant the second is legal C++, because Chapter 4.6 §3 established that an assignment is an expression with a value:
int status = 0;
if (status = 3) { std::cout << "taken\n"; }The assignment stores 3, its value is 3, and 3 converts to true. The branch runs — and status has been changed. Measured:
cond.cpp:6:16: warning: using the result of an assignment as a condition without parentheses [-Wparentheses]
cond.cpp:6:16: note: place parentheses around the assignment to silence this warning
cond.cpp:6:16: note: use '==' to turn this assignment into an equality comparisonOutput: taken.
Note that clang offers both readings rather than guessing which you meant. The second note is the fix when it was a typo. The first is the escape hatch when it was not: writing if ((status = 3)) silences the warning and records that assigning in the condition was deliberate.
The dangling else. When an if without braces contains another if, an else binds to the nearest one:
if (a > 0)
if (b > 0)
std::cout << "both positive\n";
else
std::cout << "a is not positive?\n"; // belongs to the INNER ifThe indentation says the else goes with the outer if. The language says it goes with the inner one. With a = 0 and b = 5, the measured output of the full program is:
doneNeither message printed. The outer condition was false, so the entire inner statement — including its else — was skipped. The else that was indented to handle "a is not positive" is unreachable in exactly the case it was written for.
Measured diagnostic:
dangling.cpp:9:5: warning: add explicit braces to avoid dangling else [-Wdangling-else]The fix is in the warning text. Braces make the binding explicit, and that is the whole reason §2 asked for them unconditionally.
8. What to Actually Do
Always write braces, even for a single statement. Two characters against the dangling else and against Chapter 5.7's indentation bug.
Order a chain deliberately, most specific condition first, and check the boundary values rather than the middles.
Declare a test-only variable in the if. It keeps the name where it is meaningful and out of everything below.
Say what you are testing. if (remaining != 0) over if (remaining) when the subject is a count rather than a flag.
Reach for switch when every line of a chain compares the same variable to a constant — Chapter 5.3.
9. Mistakes Worth Making Once
Writing = for == in a condition. Legal, warned, and it changes the variable as a side effect.
Trusting indentation over braces. The else binds to the nearest if, not the one you lined it up with.
Writing an else-if chain in the wrong order. Measured: swap the first two conditions and 50 becomes "medium".
Using <= where the next branch starts. Then two branches match the boundary and the first one wins, which may not be the one you wanted.
Testing the middle of each range. The middles always pass. Boundaries are where chains break.
Leaving a test-only variable in the enclosing scope. Harmless until something below reuses the name.
Assuming the init-statement form has always existed. It is C++17; under -std=c++14 it is an extension warning.
10. What You Should Be Able to Say Now
- A condition is an expression whose value selects what runs next — no new machinery beyond Module 4.
if/elseruns exactly one of two paths; never both, never neither.- The condition needs only to be convertible to
bool, and Chapter 3.6's rule says which values arefalse. - An else-if chain tests in order and stops at the first true condition, so its order is part of its meaning.
- Measured:
{50, 100, 999, 1000, 3200}gives slow / medium / medium / fast / fast. - Boundaries belong to exactly one branch when you use
<consistently; test the boundaries, not the middles. - A variable declared in a branch is not visible after it — a compile error, not a warning.
- The C++17 if-with-initializer form scopes a test-only variable to the whole
if, including itselse. - That form warns as a
-Wc++17-extensionsextension under-std=c++14. if (status = 3)assigns, is true, and warns-Wparentheses; the note asks you to say you meant it.- The dangling else binds to the nearest
if, warns-Wdangling-else, and in the measured program printed neither message. - Both of those warnings flag code whose appearance and meaning disagree, not code that computes a wrong answer.
- Braces are two characters and they remove both problems.
11. Exercises
Exercise 1 — predict the branch. For classify in §4, give the result for 99, 100, 101, 999, 1000 and 1001, and say which single condition decided each.
Exercise 2 — break the chain. Swap the first two conditions of classify and give the new output for all five inputs in §4. Explain in one sentence why the chain is now wrong.
Exercise 3 — close the gap. Rewrite classify using <= on every condition, then name the input that now lands in a different branch than before, and say which version you would ship.
Exercise 4 — scope it. Rewrite this so width does not outlive the test, using the C++17 form: int width = field_width(reg); if (width > 0) { use(width); }.
Exercise 5 — find the assignment. if (status = read_status()) compiles and warns. Say what it does, what the warning is, what it stores in status, and the two different ways to make the line say what you mean.
Exercise 6 — bind the else. Add braces to §7's dangling-else example two different ways: one where the else belongs to the outer if, one where it belongs to the inner. Give the output of each for a = 0, b = 5.
Exercise 7 — chain or switch? For each, say whether an else-if chain or a switch fits better and why: (a) opcode equals 1, 2 or 3 (b) mhz falls in one of four ranges (c) ready is true and error is false (d) command is 'r', 'w' or 'x'.
12. Summary
- A condition is an expression whose value selects what runs next — Module 4 supplied everything but the choosing.
if/elseruns exactly one path. Braces are optional; write them.- The condition needs only to convert to
bool. - An else-if chain stops at the first true condition, so its order carries meaning and its boundaries need checking.
- Measured: 100 is "medium" and 1000 is "fast" under consistent
<. - A variable declared in a branch is gone after it — a compile error.
- C++17's if-with-initializer scopes a test-only variable to the
ifand itselse; it warns as an extension under C++14. if (x = 3)assigns and is true, and warns-Wparentheses.- The dangling else binds to the nearest
if, warns-Wdangling-else, and printed nothing in the measured case. - Both warnings mark a gap between appearance and meaning rather than a wrong computation.
The next chapter takes the same decision and makes it an expression instead of a statement. That sounds like a small repackaging, and it comes with a type rule that catches people out: the result's type is decided by both branches together, not by the branch that actually runs — which means a conditional can hand you a value of a type neither branch would have produced alone, and Chapter 4.10's conversions are waiting underneath it.
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.
