Phase 1 · Modern C++ FoundationsModule 4~40 min read

Decisions & Control Flow

Make programs choose safely with Boolean logic, if/else, switch, scoped enums, short-circuiting, and conditional expressions.

What you'll learn

Decisions turn data into behavior. C++ offers simple branches, multi-way selection, scoped domain choices, and short-circuit operators—but readable conditions and explicit invariants matter more than clever expressions.

By the end, you'll be able to:

  • Build complete decision chains with if, else if, and else
  • Combine and guard conditions with Boolean operators
  • Use decision initializers to keep temporary state local
  • Model fixed choices with enum class and switch
Every decision selects a control-flow path
condition
true
execute the selected branch
false
try another branch or continue

Conditions & bool

A condition is contextually converted to bool. Comparisons already produce a Boolean result, so write if (ready) instead of if (ready == true).

ExpressionMeaning
left == rightequal values
left != rightdifferent values
left < rightleft is less
value >= low && value <= highinclusive range
!readylogical negation

Watch out

Assignment uses =; equality uses ==. An assignment can sometimes appear as a condition, so keep warnings enabled and avoid burying assignments inside tests.

if, else if & else

An if chain evaluates conditions from top to bottom and executes only the first matching branch. Order overlapping tests from most specific or highest threshold to least.

grades.cpp
#include <iostream>

int main() {
    int score{87};

    if (score >= 90) {
        std::cout << "Grade A\n";
    } else if (score >= 80) {
        std::cout << "Grade B\n";
    } else if (score >= 70) {
        std::cout << "Grade C\n";
    } else {
        std::cout << "Keep practicing\n";
    }
}

Key idea

The final else expresses the complete remaining case. Add it when every input needs an outcome; omit it when doing nothing is a deliberate valid result.

Logical operators

&& requires both conditions, || requires at least one, and! negates a Boolean value. Name an important compound rule when doing so makes the code read like the domain.

admission.cpp
#include <iostream>

int main() {
    int age{22};
    bool has_ticket{true};
    bool suspended{false};

    bool admitted{age >= 18 && has_ticket && !suspended};
    bool needs_help{age < 18 || !has_ticket};

    std::cout << std::boolalpha
              << admitted << ' ' << needs_help << '\n';
}
ABA && BA || B
falsefalsefalsefalse
falsetruefalsetrue
truefalsefalsetrue
truetruetruetrue

Short-circuit guards

C++ evaluates logical operands left to right. For &&, a false left operand decides the result; for ||, a true left operand decides it. The right side is then skipped.

guarded_division.cpp
#include <iostream>

int main() {
    int total{120};
    int count{0};

    // count != 0 is evaluated first. Division runs only when safe.
    if (count != 0 && total / count >= 60) {
        std::cout << "Passing average\n";
    } else {
        std::cout << "No passing average available\n";
    }
}

Watch out

Put the safety check first. Reversing the conditions would attempt total / countbefore discovering that count is zero.

Decision initializers

C++17 and later allow an initializer before the condition in if andswitch. The introduced name stays inside the whole decision, minimizing scope.

if_initializer.cpp
#include <iostream>

int main() {
    if (int score{84}; score >= 60) {
        std::cout << "Passed with " << score << '\n';
    } else {
        std::cout << "Not yet: " << score << '\n';
    }

    // score is not visible here; its scope is the entire if statement.
}

Tip

Use this form when a temporary exists only to support one decision—for example, a lookup result, parse result, lock, or status value.

switch statements

switch selects among constant integral or enumeration cases. It is a good fit for one value with several discrete choices, not numeric ranges or unrelated conditions.

commands.cpp
#include <iostream>

int main() {
    char command{'s'};

    switch (command) {
        case 'n':
            std::cout << "New document\n";
            break;
        case 's':
            std::cout << "Save document\n";
            break;
        case 'q':
            std::cout << "Quit\n";
            break;
        default:
            std::cout << "Unknown command\n";
            break;
    }
}

Watch out

Without break, execution continues into the next case. When fallthrough is intentional, state it with [[fallthrough]];; otherwise terminate the branch.

Scoped enumerations

An enum class creates a distinct type with scoped names. Unlike an unscoped enum, its values do not leak into the surrounding scope or convert implicitly to integers.

traffic_light.cpp
#include <iostream>

enum class TrafficLight { red, yellow, green };

int main() {
    TrafficLight light{TrafficLight::green};

    switch (light) {
        case TrafficLight::red:
            std::cout << "Stop\n";
            break;
        case TrafficLight::yellow:
            std::cout << "Prepare to stop\n";
            break;
        case TrafficLight::green:
            std::cout << "Go\n";
            break;
    }
}

Key idea

Use enum class when a value must be exactly one choice from a small known set. The type prevents mixing unrelated concepts that happen to use the same underlying numbers.

Conditional expressions

The conditional operator chooses one of two expressions and produces a value. It is useful for a short local choice; use an if statement when branches require multiple actions or nested conditions.

conditional.cpp
#include <iostream>
#include <string_view>

int main() {
    int items{3};
    std::string_view noun{items == 1 ? "item" : "items"};
    int fee{items > 5 ? 0 : 4};

    std::cout << items << ' ' << noun
              << ", delivery fee: $" << fee << '\n';
}

Common pitfalls

  • Always use braces, even for one-statement branches
  • Do not compare computed floating-point values with exact equality
  • Avoid comparing signed negative values with unsigned sizes
  • Keep conditions positive and named when several negations obscure meaning
  • Do not repeat an expensive or state-changing function call in several branches
float_comparison.cpp
#include <cmath>
#include <iostream>

bool approximately_equal(double left, double right, double tolerance) {
    return std::abs(left - right) <= tolerance;
}

int main() {
    double result{0.1 + 0.2};
    std::cout << std::boolalpha
              << approximately_equal(result, 0.3, 1e-12) << '\n';
}

Note

A useful floating-point tolerance depends on scale and domain. The absolute tolerance above teaches the idea; scientific and financial applications need a policy designed for their numeric requirements.

Recap & quick check

Key takeaways

  • An if chain executes only the first matching branch.
  • Logical operators combine Boolean rules and short-circuit from left to right.
  • Decision initializers keep temporary state within the decision's scope.
  • switch fits discrete integral or enumeration choices; enum class creates a strong scoped domain type.
  • The conditional operator chooses a value; statements handle larger behavioral branches.

Quick check

1. Why must count != 0 appear first in count != 0 && total / count > 5?

2. What does enum class primarily provide?

3. When is switch a good fit?

4. What is the scope of a variable declared in an if initializer?

Your programs can now choose deliberately. Next: Module 5 — Loops & Iteration, where the same reasoning controls repeated work.