20 topics
/
← Back to Quick Reference
Topic 06

If / Else

Initializer if · if constexpr · Short-Circuit · Ternary · std::optional

C++17 · Advanced Reference

if / else Fundamentals

01

Branching 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. 1.Always use braces {} — even for single-statement bodies. Prevents dangling-else and accidental scope bugs.
  2. 2.= inside a condition is assignment, not comparison. Enable -Wall to catch this.
  3. 3.The else always binds to the nearest unmatched if — braces make this unambiguous.
  4. 4.Conditions are evaluated left-to-right with short-circuit semantics.

What counts as true / false

  1. 1.false: 0, 0.0, nullptr, false
  2. 2.true: any non-zero integer, non-null pointer, true
  3. 3.Standard containers (vector, string) do NOT implicitly convert to bool — use .empty()
  4. 4.std::optional does convert to booltrue if 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_guardScoped 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-timeCondition must be a compile-time constant expression. Selected branch is instantiated; discarded branch is not.
vs regular ifRegular if compiles both branches for every template instantiation — fails if a branch uses a type-specific API.
static_assertSafe 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->xSafe: 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 || costlyPut the fast check first. If it short-circuits, the slow check never runs.
side effectsAvoid 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::nulloptRepresents 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->mDirect 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.