20 topics
← Back to Quick Reference/
Topic 06
IIff // EEllssee
Initializer if · if constexpr · Short-Circuit · Ternary · std::optional
C++17 · Advanced Referenceif / else Fundamentals
01Branching in C++
if evaluates a condition and executes one of two branches. C++ considers any non-zero scalar value as true — integers, pointers, and floats all work as conditions, though only bool expresses intent clearly.
Rules
- 1.Always use braces
{}— even for single-statement bodies. Prevents dangling-else and accidental scope bugs. - 2.
=inside a condition is assignment, not comparison. Enable-Wallto catch this. - 3.The
elsealways binds to the nearest unmatchedif— braces make this unambiguous. - 4.Conditions are evaluated left-to-right with short-circuit semantics.
What counts as true / false
- 1.
false:0,0.0,nullptr,false - 2.
true: any non-zero integer, non-null pointer,true - 3.Standard containers (
vector,string) do NOT implicitly convert tobool— use.empty() - 4.
std::optionaldoes convert tobool—trueif it holds a value
Prefer explicit comparisons (x != 0) over implicit truthiness for non-bool types — it signals intent clearly.
// ── Standard if / else if / else ──────────────────────────── if (x > 0) { std::cout << "positive\n"; } else if (x == 0) { std::cout << "zero\n"; } else { std::cout << "negative\n"; } // ── Common pitfalls ────────────────────────────────────────── if (x = 5) // ❌ assignment, not comparison — always true! if (x == 5) // ✅ comparison // Dangling else — the else binds to the nearest if if (a) if (b) doB(); else // ← this else belongs to "if (b)", NOT "if (a)" doElse(); // Fix: always use braces if (a) { if (b) doB(); } else { doElse(); }
if with Initializer (C++17)
02// if with initializer (C++17) // Syntax: if (init-statement; condition) // The initialized variable lives only for the if/else block // File open check if (std::ifstream file{"data.txt"}; file) { std::string line; std::getline(file, line); // file is in scope here } // file closed and destroyed here // Map lookup if (auto it = map.find(key); it != map.end()) { use(it->second); } // it destroyed here — no namespace pollution // Mutex lock if (std::lock_guard lock{mutex}; condition) { // critical section } // vs. the old way (variable leaks into outer scope): auto it = map.find(key); // still visible after the if if (it != map.end()) { use(it->second); }
| if (init; cond) | Init runs once before the condition. Variable is scoped to the entire if/else block — not the surrounding scope. |
| map.find() | Classic use case: scope the iterator to the lookup — avoids a dangling variable after the block. |
| lock_guard | Scoped lock held only for the duration of the if/else body. |
Reduces namespace pollution.Without the initializer form, helper variables like iterators and file handles leak into the surrounding scope even though they're only relevant to the branch.
if constexpr (C++17)
03#include <type_traits> // if constexpr (C++17) — compile-time branch selection // The discarded branch is parsed but NOT instantiated // → no type errors in the dead branch template<typename T> void process(T val) { if constexpr (std::is_integral_v<T>) { std::cout << "int: " << val << "\n"; } else if constexpr (std::is_floating_point_v<T>) { std::cout << "float: " << std::fixed << val << "\n"; } else if constexpr (std::is_same_v<T, std::string>) { std::cout << "str[" << val.size() << "]: " << val << "\n"; } else { static_assert(false, "unsupported type"); } } // Key difference from regular if: // Regular if — both branches compiled for every T (fails if T // doesn't support .size() when T=int) // if constexpr — only matching branch compiled for each T
| compile-time | Condition must be a compile-time constant expression. Selected branch is instantiated; discarded branch is not. |
| vs regular if | Regular if compiles both branches for every template instantiation — fails if a branch uses a type-specific API. |
| static_assert | Safe to put in the else branch — only fires when that branch is actually instantiated. |
Replaces most SFINAE and tag dispatch.
if constexpr is the modern, readable way to write type-dependent logic inside a single function template.Short-Circuit Evaluation
04// && short-circuits: stops at first false // || short-circuits: stops at first true // Safe null-pointer check if (ptr != nullptr && ptr->value > 0) { } // ^^^^^^^^^^^^^^^^ if false, ptr->value never touched // Safe array access if (i < vec.size() && vec[i] == target) { } // Lazy evaluation — expensive() only runs when needed if (cheapCheck() || expensive()) { } // Short-circuit with side effects — be careful int x = 0; if (true || ++x) { } // x is still 0 — ++x never ran // Use this pattern for optional initialization: Node* node = (head != nullptr) ? head->next : nullptr; // ── Truthiness rules ───────────────────────────────────────── // False: 0, 0.0, nullptr, false, empty (NOT automatic for objects) // True: anything else // std::string, std::vector etc. do NOT convert to bool implicitly // Use: if (!str.empty()) not if (str)
| ptr && ptr->x | Safe: right side only evaluated if ptr is non-null. The canonical null-guard pattern. |
| i < n && a[i] | Safe: bounds check before access. Order matters — flip them and you get UB. |
| cheap || costly | Put the fast check first. If it short-circuits, the slow check never runs. |
| side effects | Avoid side effects (++, function calls) in the right operand of && / || — they may silently not execute. |
Ternary Operator
05// condition ? value_if_true : value_if_false int abs_x = (x >= 0) ? x : -x; // Both branches must be the same type (or implicitly convertible) auto val = flag ? 1 : 2.0; // int promoted to double → double // Nested ternary (chain, right-associative) — use sparingly std::string grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : "F"; // Ternary in initializer (avoids if/else for const init) const std::string msg = (err == 0) ? "ok" : "fail"; // Without ternary, msg can't be const (can't assign in if/else after decl) // ⚠ Ternary cannot contain statements — only expressions // (x > 0) ? doA() : doB(); // ok if doA/doB return values // (x > 0) ? { doA(); } : doB(); // ❌ braces not allowed
Best for single-expression choices. Use ternary when initializing a
const or choosing between two values inline. Prefer if/else for anything involving statements, side effects, or more than two branches.std::optional as a Branch Alternative
06#include <optional> // std::optional<T> — a value that may or may not exist // Better than returning -1 or nullptr as a sentinel std::optional<int> divide(int a, int b) { if (b == 0) return std::nullopt; // no value return a / b; } // Using the result if (auto result = divide(10, 2); result) { std::cout << *result << "\n"; // dereference to get value } // Other access patterns auto r = divide(10, 0); r.has_value(); // false r.value_or(-1); // -1 — safe fallback, no exception *r; // ❌ undefined behavior if empty — use has_value first // optional as early-return guard (replaces nested ifs) std::optional<Config> cfg = loadConfig(); if (!cfg) { return; } // bail early use(cfg->setting); // -> works on optional directly
| std::nullopt | Represents the empty state. Assign to optional to indicate no value. |
| value_or(x) | Returns the value if present, otherwise x. Never throws. Preferred over dereferencing. |
| *opt / opt->m | Direct access — UB if empty. Only use after has_value() or inside an if (opt) check. |
| if (init; opt) | Combine with C++17 if-initializer to scope the optional and test it in one line. |
Replace sentinel values. Returning
-1, nullptr, or INT_MAX to signal failure is error-prone. std::optionalmakes the "no result" case explicit in the type system.