Conditionals
C++ inherits C’s if, if/else, conditional operator, and switch and adds two notable extensions: if-with-initialiser (C++17), which combines an initialisation step with the condition, and if constexpr (C++17), which selects branches at compile time and discards the unselected ones. Both are central to modern idioms — the first replaces the awkward “declare-then-check” pattern that C requires, the second replaces a substantial fraction of the SFINAE machinery used in pre-C++17 generic code.
The control-flow surface beyond these is essentially C’s. The switch statement is the same integer-dispatch mechanism, with the same fallthrough behaviour and the same C++11 attribute ([[fallthrough]]) for documenting intentional fallthroughs.
if and else
The basic if statement evaluates its controlling expression and executes the body if the result compares unequal to zero (or false):
if (n > 0) {
process(n);
}
The body may be any statement, but the conventional form uses a compound statement (block) even for a single line. The else clause attaches to the immediately preceding if:
if (x > 0) {
classify_positive(x);
} else if (x < 0) {
classify_negative(x);
} else {
classify_zero();
}
C++ does not have elif; else if is two keywords, and the parser treats it as else { if … } with elided braces.
Truthy and falsy
The controlling expression need not be of bool type. Any scalar value is acceptable; zero is treated as false, non-zero as true. Pointers compare against nullptr:
if (count) // count != 0
if (p) // p != nullptr
if (auto found = find(c, x); found != c.end()) // see below
The convention to test pointers and counts directly without an explicit != nullptr or != 0 is widespread; the reverse convention — always make the test explicit — is also defensible and is the convention in some style guides.
if-with-initialiser
C++17 introduced the if-with-initialiser form, which combines an initialisation step with the condition:
if (auto it = map.find(key); it != map.end()) {
use(it->second);
}
The initialisation auto it = map.find(key) is performed first; the result it is in scope for both the condition and the if/else bodies; once the if block ends, it goes out of scope. The construction is the C++ analogue of the C idiom if ((it = ...) != end) { ... } but is type-safe, uses the modern named-cast forms, and limits the variable’s scope.
The conventional uses:
// Container search
if (auto it = container.find(key); it != container.end()) { /* found */ }
// I/O
if (auto file = std::ifstream{path}; file.is_open()) { /* readable */ }
// Lock acquisition with try_lock
if (auto guard = std::unique_lock{m, std::try_to_lock}; guard.owns_lock()) { /* held */ }
// Optional access
if (auto opt = compute_optional(); opt.has_value()) { /* present */ }
The pattern keeps the variable scoped to the block where it is used, eliminating the surrounding-scope pollution that the older form created.
if constexpr
C++17 introduced constexpr if, which evaluates its condition at compile time and discards the unselected branch:
template <typename T>
void process(T value) {
if constexpr (std::is_integral_v<T>) {
std::cout << "integer: " << value << '\n';
} else if constexpr (std::is_floating_point_v<T>) {
std::cout << "float: " << value << '\n';
} else {
std::cout << "other\n";
}
}
The discarded branch is not compiled in the instantiation. The mechanism makes the body of the un-taken branch invisible to the compiler — code that would not compile for a particular T is permitted in branches discarded for that T:
template <typename T>
void serialize(T value) {
if constexpr (std::is_arithmetic_v<T>) {
binary_write(value); // requires arithmetic operations
} else if constexpr (std::is_class_v<T>) {
value.serialize(); // requires the member function
}
// For T = std::string, neither branch's body is compiled;
// the compiler does not need binary_write or .serialize() to be valid for std::string.
}
Before C++17, the same effect required SFINAE-based overload selection, which was substantially more verbose and produced harder-to-read code. if constexpr is the modern C++ mechanism for compile-time discrimination based on type traits or other constant expressions.
The else and else if of an if constexpr may also be constexpr, producing a compile-time chain.
The conditional operator
The ternary ?: operator selects between two expressions:
int max = (a > b) ? a : b;
auto plural = (n == 1) ? "" : "s";
The two result expressions must have a common type, found by the standard’s elaborate rules; mixing types unwisely produces compilation errors. The construction is conventional for short selections inside larger expressions.
Nested conditionals are syntactically legal but quickly become unreadable; one or two levels is typically the practical limit.
The conditional operator returns an lvalue when both arms are lvalues of the same type — a legacy of C++‘s expression-orientation:
int a = 0, b = 0;
((a < b) ? a : b) = 42; // assigns to whichever is smaller; legal C++
The construction is rare in modern code; clearer forms are preferred.
switch
The switch statement compares its integer-typed expression against integer-constant labels:
switch (op) {
case '+': result = a + b; break;
case '-': result = a - b; break;
case '*': result = a * b; break;
case '/':
if (b == 0) return -1;
result = a / b;
break;
default:
return -1;
}
The mechanics are inherited from C: the controlling expression must have integer type after the integer promotions; each case label must be an integer constant expression; fallthrough between cases is implicit unless break is reached.
Fallthrough and [[fallthrough]]
Implicit fallthrough between substantive cases is a frequent source of bugs. The [[fallthrough]] attribute (C++17) marks intentional fallthrough explicitly:
switch (state) {
case STATE_IDLE:
prepare();
[[fallthrough]]; // intentional
case STATE_RUNNING:
execute();
break;
case STATE_STOPPED:
cleanup();
break;
}
Compilers with -Wimplicit-fallthrough (or the equivalent) warn about un-annotated fallthroughs. The combination of the warning and the attribute catches most accidental cases at compile time.
Switch with initialiser
C++17’s switch-with-initialiser form combines initialisation with the discriminant:
switch (auto e = next_event(); e.kind) {
case EVENT_OPEN: handle_open(e); break;
case EVENT_CLOSE: handle_close(e); break;
}
The e variable is in scope for the entire switch; the construction is the parallel of if-with-initialiser.
Scoped enumerations
A switch on a scoped enumeration is the conventional discrimination on a closed set of states:
enum class Direction { North, East, South, West };
const char *name(Direction d) {
switch (d) {
case Direction::North: return "north";
case Direction::East: return "east";
case Direction::South: return "south";
case Direction::West: return "west";
}
return "unknown";
}
Omitting default and listing every enumerator lets -Wswitch diagnose newly added enumerators that the switch does not handle. The mechanism is the closest C++ comes to compile-time exhaustiveness checking on enumerated discrimination.
Selection idioms
Several patterns recur in idiomatic C++ control flow.
Early return
int parse(const std::string &input, Parsed &out) {
if (input.empty()) return -1;
if (input.size() > MAX_INPUT) return -2;
if (!is_valid(input)) return -3;
/* main body */
return 0;
}
The pattern reduces nesting and keeps the precondition checks visually separate from the substantive body.
if-with-initialiser to avoid scope pollution
// Old:
auto it = map.find(key);
if (it != map.end()) {
use(it->second);
}
// it is still in scope here — pollution
// New:
if (auto it = map.find(key); it != map.end()) {
use(it->second);
}
// it is out of scope here — clean
if constexpr for type-based dispatch
template <typename T>
auto absolute(T value) {
if constexpr (std::is_unsigned_v<T>) {
return value;
} else {
return value < 0 ? -value : value;
}
}
The construction replaces type-trait-driven SFINAE selection in pre-C++17 code.
std::optional and std::variant for absence and disjunction
When a value may be absent, std::optional is the conventional carrier; the test combines well with if-with-initialiser:
if (auto result = compute_optional(); result.has_value()) {
use(*result);
}
When a value is one of several alternatives, std::variant and std::visit provide a discriminated dispatch covered in Pattern matching.
A note on goto
C++ retains goto from C; it is rare in idiomatic code. The principal use in C — the cleanup-label idiom — is replaced in C++ by RAII and destructors: every resource acquired in scope is released automatically at scope exit. The cases that remain (multi-level loop exit, hand-written state machines transcribed from a diagram) are uncommon, and most are better expressed via a function or via a control-flow restructuring.