A condition chooses which statement runs next. C++ contextually converts an if condition to bool, tests an else-if chain from top to bottom, and executes only the first matching branch.
Use if for ranges and compound predicates, guard clauses for invalid or exceptional paths, and switch for one integral or enumeration value with discrete cases. Make every boundary and fallthrough decision visible.
The expression inside if must be usable as a Boolean condition. Comparisons produce bool directly, while numeric and pointer values can also be contextually converted: zero and null are false, and nonzero values and non-null pointers are true.
Prefer an explicit comparison when it communicates intent. if (count > 0) explains a quantity rule more clearly than relying on the truthiness of count, while if (pointer) is a familiar null check.
| Construct | Best fit | Important rule |
|---|---|---|
| if | One predicate or independent decision | Runs its body only when the condition is true. |
| else if | Ordered, mutually exclusive alternatives | Stops after the first true branch. |
| else | Fallback for every unmatched value | Has no condition and belongs to the nearest unmatched if. |
| guard clause | Invalid input or exceptional path | Returns, continues, or throws early so the normal path stays shallow. |
| switch | One integral or enum value with named cases | Case labels are constant values; exit or fallthrough must be intentional. |
An if-else-if chain is evaluated in source order. Put invalid input first, then arrange overlapping ranges from most specific to broadest so an earlier branch does not hide a later one.
Use braces even for a one-line body when the branch may grow. Braces prevent a later edit from creating a misleading indentation or dangling-else bug.
#include <iostream>
#include <string_view>
std::string_view category(int age) {
if (age < 0) {
return "invalid";
} else if (age < 13) {
return "child";
} else if (age < 18) {
return "teen";
} else {
return "adult";
}
}
int main() {
std::cout << category(-1) << "\n";
std::cout << category(12) << "\n";
std::cout << category(13) << "\n";
std::cout << category(18) << "\n";
}
invalid
child
teen
adult
The first matching branch wins. Testing -1, 12, 13, and 18 proves the invalid boundary and every transition between adjacent ranges.
Combine predicates with && for AND, || for OR, and ! for NOT. The && and || operators short-circuit from left to right: C++ skips the right operand when the left operand already determines the result.
Put the safety check before the operation it protects. This is useful for pointer access, divisors, indexes, and operations that should run only after validation. Avoid hiding unrelated side effects inside a condition.
#include <iostream>
#include <string>
struct User {
bool active;
std::string role;
};
bool mayConfigure(const User* user) {
return user != nullptr
&& user->active
&& user->role == "admin";
}
int main() {
User admin{true, "admin"};
User viewer{true, "viewer"};
std::cout << std::boolalpha;
std::cout << mayConfigure(nullptr) << "\n";
std::cout << mayConfigure(&admin) << "\n";
std::cout << mayConfigure(&viewer) << "\n";
}
false
true
false
When user is null, && stops immediately, so the program never evaluates user->active and does not dereference a null pointer.
A guard clause handles a condition that prevents normal work from continuing. Returning early reduces nesting and lets the remainder of the function describe the successful path.
A guard should state one clear reason to stop. Do not scatter partially completed state changes before validation; validate first or use an operation with a clear rollback boundary.
#include <iostream>
bool withdraw(int& balance, int amount) {
if (amount <= 0) {
return false;
}
if (amount > balance) {
return false;
}
balance -= amount;
return true;
}
int main() {
int balance = 500;
std::cout << std::boolalpha;
std::cout << withdraw(balance, -20) << "\n";
std::cout << withdraw(balance, 200) << "\n";
std::cout << balance << "\n";
}
false
true
300
Both failure paths return before balance changes. The successful path has no extra else block and performs the mutation only after validation.
switch compares one integral or enumeration value against constant case labels. It cannot directly express ranges, compound predicates, or std::string comparisons, so those decisions belong in an if chain or a prior conversion step.
A matching case continues into later cases until control exits. Use break, return, or throw for an independent case; stack labels when several values share behavior; and use [[fallthrough]] when execution must deliberately continue into the next case.
| Switch feature | Rule |
|---|---|
| case label | Must be a unique constant expression compatible with the switch value. |
| break | Exits the switch; omitting it permits fallthrough. |
| return or throw | Exits the function or transfers control, so break is unnecessary. |
| default | Handles values not matched by a case; consider omitting it for a closed enum when compiler warnings should reveal missing enumerators. |
| initialization statement | Modern C++ permits setup before the condition, such as switch (auto code = read(); code). The name is scoped to the switch. |
#include <iostream>
#include <string_view>
enum class TrafficLight {
red,
yellow,
green
};
std::string_view action(TrafficLight light) {
switch (light) {
case TrafficLight::red:
return "stop";
case TrafficLight::yellow:
return "prepare";
case TrafficLight::green:
return "go";
}
return "unknown";
}
int main() {
std::cout << action(TrafficLight::red) << "\n";
std::cout << action(TrafficLight::green) << "\n";
}
stop
go
Each enumerator has one visible outcome and returns directly, so accidental fallthrough is impossible. Enabling compiler warnings helps reveal a newly added enumerator that the switch does not yet handle.
Choose the construct that makes the decision easiest to verify. Readability comes from visible boundaries and one clear selection rule, not from minimizing line count.
| Decision shape | Prefer | Reason |
|---|---|---|
| Range, string, pointer, or compound predicate | if / else if | Conditions can use relational and logical operators. |
| Invalid input before normal work | guard clause | Failure exits early and reduces nesting. |
| One enum or integral value with discrete labels | switch | Cases make the finite alternatives easy to scan. |
| Choose one of two simple values | conditional operator ?: | Useful inside an expression when both alternatives remain short and readable. |
| Map many values to data | table or associative container | Data-driven lookup may be clearer than a long branch chain. |
0 of 3 checked
Apply the selection rules
0 of 3 completed
Cases fall through unless control exits with <code>break</code>, <code>return</code>, <code>throw</code>, or another jump. Add an exit for independent cases and use <code>[[fallthrough]]</code> when continuing is intentional.
Without braces, <code>else</code> belongs to the nearest unmatched <code>if</code>, regardless of indentation. Braces make the intended ownership explicit and safer to edit.
<code>switch</code> handles integral or enum values using constant equality-style labels. It cannot directly express ranges, compound predicates, or string comparisons.
Explore 500+ free tutorials across 20+ languages and frameworks.