Lesson 7 of 25

Conditionals

if / else if / else, and Why Order Matters

A conditional is how a program stops being a fixed recipe and starts responding to its data. The mechanics are simple — evaluate a condition, run one block if it is true and another if it is not — so the interesting part is not the syntax but the two ways people reliably get it wrong.

The first is order. An if / else if / else chain is evaluated strictly from the top, and execution stops at the first condition that is true; every branch below it is skipped without being examined. That makes a grading ladder correct only if it runs from the strictest condition downwards. Put score >= 40 at the top and every passing student, including the one with 95, is labelled a third division, because their score satisfies that first test and the chain never looks any further. The bug produces no error and no warning. It simply gives quietly wrong answers.

The second is forgetting that else if is a chain, not a set of independent tests. Writing a series of separate if statements makes every condition run, so overlapping ranges match more than once and each match executes. Sometimes that is exactly what you want — several independent checks on a form, for instance — but if the cases are meant to be mutually exclusive, chain them.

Once a chain reaches three or four branches, ask yourself whether the conditions really are mutually exclusive and whether the ordering is enforced by the structure or only by luck. In the grading example the ordering is load-bearing: swap two branches and the program is wrong. A comment saying "highest first" is worth writing.

Example
#include <iostream>
#include <string>

std::string division(int score) {
    // Strictest condition first — the order is load-bearing.
    if (score >= 75)      return "Distinction";
    else if (score >= 60) return "First";
    else if (score >= 45) return "Second";
    else if (score >= 33) return "Third";
    else                  return "Fail";
}

int main() {
    std::cout << division(95) << '\n';   // Distinction
    std::cout << division(46) << '\n';   // Second

    // The bug: loosest condition first. Everyone becomes "Third".
    // if (score >= 33)      return "Third";
    // else if (score >= 75) return "Distinction";   // unreachable in practice

    // Independent checks — every one of these should run
    std::string password = "abc";
    if (password.size() < 8)      std::cout << "too short\n";
    if (password.find(' ') != std::string::npos) std::cout << "no spaces allowed\n";
}
Notes
  • The ternary condition ? a : b is the expression form of a two-way choice, and it is the only way to conditionally initialise a const: const std::string result = (score >= 33) ? "Pass" : "Fail";. Keep it to one simple decision — nested ternaries are far harder to read than the if they replaced.

Braces, Scope and the C++17 if-initialiser

C++ lets you omit the braces when a branch contains a single statement. It is legal, it looks tidy, and it has caused enough real-world bugs that most professional style guides now forbid it. The problem is that indentation means nothing to the compiler: add a second line under a brace-less if, indent it to match, and it runs unconditionally while looking as though it does not. Nobody notices in review because it reads correctly.

The related classic is the dangling else. When you nest a brace-less if inside another if and then write an else, that else attaches to the nearest unmatched if, not the one your indentation suggests. Braces make the intent explicit and the question disappears. Write them always, even for one line.

Braces do a second job: they create a scope. A variable declared inside an if block exists only until the closing brace, at which point its destructor runs and its name is free again. This is a feature to use deliberately — declaring a variable in the smallest scope that needs it means it cannot be accidentally read later, and the compiler will tell you if you try.

C++17 added a neat extension of this idea, the if with initialiser: if (init; condition). It lets you declare a variable, test it, and confine it to the if in one statement. The classic use is a container lookup, where you want the iterator only if the search succeeded. It reads better and, more importantly, it stops a stale iterator from being visible in the rest of the function.

Example
#include <iostream>
#include <map>
#include <string>

int main() {
    int marks = 20;

    // Looks like both lines are conditional. Only the first one is.
    // if (marks > 50)
    //     std::cout << "passed\n";
    //     std::cout << "congratulations\n";   // ALWAYS runs

    if (marks > 50) {
        std::cout << "passed\n";
        std::cout << "congratulations\n";
    }

    // Scope: `bonus` cannot leak out of the block
    if (marks > 90) {
        int bonus = 5;
        std::cout << marks + bonus << '\n';
    }
    // std::cout << bonus;   // error: 'bonus' was not declared in this scope

    // C++17: declare, test, and scope in one statement
    std::map<std::string, int> rollNumbers = {{"Ananya", 118}, {"Rahul", 119}};
    if (auto it = rollNumbers.find("Ananya"); it != rollNumbers.end()) {
        std::cout << it->second << '\n';    // 118
    } else {
        std::cout << "not enrolled\n";
    }
    // `it` no longer exists here — it cannot be used by mistake
}
Notes
  • The habit of always bracing is cheap insurance rather than pedantry. A widely reported iOS and macOS security flaw in 2014, nicknamed "goto fail", came down to one duplicated line under a brace-less if that skipped a certificate check.

switch: What It Is Good For, and Its Two Traps

switch compares one value against a list of constants and jumps to the matching label. Its natural home is a menu, a command code, a state machine, or an enum — situations where you are dispatching on one of a fixed, known set of discrete values. When the branches are ranges or complex conditions, switch cannot express them and an if chain is the right tool.

The first restriction to know is what you are allowed to switch on: an integer type, a char, or an enumeration. You cannot switch on a std::string or a double. Students coming from Java or Python find this surprising. The reason is that switch is meant to compile down to a jump table or a tight comparison chain, and that requires values the compiler can reason about at compile time. Each case label must likewise be a compile-time constant, not a variable.

The first trap is fall-through. A case is a label, not a block, so once execution jumps there it keeps running straight through the following cases until it meets a break or the end of the switch. Forget one break and a menu option silently performs its own action and the next one too. Deliberate fall-through is genuinely useful for grouping labels that share a body, and since C++17 you can mark an intentional one with [[fallthrough]]; so both the compiler and the next reader know you meant it.

The second trap is declaring a variable inside a case without braces. Because all the cases share one scope, jumping to a later label would skip over that variable's initialisation while it remains in scope — so the compiler rejects it with a message about crossing initialisation. The fix is to wrap that case's body in its own braces. And always write a default, even if it only reports an unexpected value: a switch with no default silently does nothing when the input is not one you planned for.

Example
#include <iostream>

int main() {
    int choice = 2;

    switch (choice) {
        case 1:
            std::cout << "Check balance\n";
            break;

        case 2: {                       // braces: this case declares a variable
            double amount = 500.0;
            std::cout << "Withdraw " << amount << '\n';
            break;
        }

        // Deliberate grouping: 3 and 4 share one body
        case 3:
        case 4:
            std::cout << "Transfer\n";
            break;

        case 5:
            std::cout << "Logging usage\n";
            [[fallthrough]];            // C++17: yes, this is on purpose
        case 6:
            std::cout << "Exit\n";
            break;

        default:
            std::cout << "Unknown option\n";
            break;
    }

    // Not allowed: switch (someString) { ... }
    // switch works on integers, chars and enums only.
}
Notes
  • switch pairs beautifully with an enum class. Most compilers will warn you, with -Wall, when a switch over an enum forgets one of its values — so adding a new state to the enum makes the compiler point at every switch that needs updating. That is a maintenance win an if chain cannot give you.

Conditions That Are Not Really Booleans

A condition in C++ does not have to be a bool. Anything that can convert to one will do, and the rule is that zero is false and everything else is true. This is why if (n) means "if n is not zero" and if (!n) means "if n is zero". Both are common and both are fine once you know the rule, though spelling out if (n != 0) is friendlier to a reader who does not.

Where this bites is floating point. Testing if (x == 0.3) after computing 0.1 + 0.2 fails, because binary floating point cannot represent those decimals exactly and the sum lands a hair away from 0.3. This is not a C++ quirk; it is how the hardware works, and every language with floating point has it. Compare with a tolerance instead: check that the absolute difference is smaller than some small epsilon you choose based on the magnitudes involved.

Pointers convert to bool too — a null pointer is false, any other value is true — which is why if (ptr) is idiomatic. Standard library types generally do not. std::string has no conversion to bool, so if (name) will not compile; you want if (!name.empty()). Prefer .empty() over size() == 0 throughout the standard library: it says what you mean, and for some containers it is faster.

One last habit worth building: prefer positive conditions where you can. if (!isNotReady) is technically correct and takes a moment of thought to read; if (isReady) takes none. Naming a bool for the true case and testing it directly removes a whole category of reading errors.

Example
#include <cmath>
#include <iostream>
#include <string>

int main() {
    int count = 0;
    if (!count) std::cout << "nothing yet\n";      // same as count == 0

    // Floating point equality: do not do this
    double sum = 0.1 + 0.2;
    if (sum == 0.3) std::cout << "exact\n";        // does NOT print

    // Compare with a tolerance instead
    const double eps = 1e-9;
    if (std::fabs(sum - 0.3) < eps) std::cout << "close enough\n";   // prints

    // Pointers convert to bool; std::string does not
    int* p = nullptr;
    if (!p) std::cout << "null pointer\n";

    std::string name;
    // if (name) { }             // error: no conversion to bool
    if (name.empty()) std::cout << "no name given\n";

    // Read the positive form
    bool isEnrolled = true;
    if (isEnrolled) std::cout << "welcome\n";
}
Notes
  • Never use floating point for money, and therefore never compare money with ==. Store rupees as paise in a long long, do exact integer arithmetic, and divide by 100 only at the moment you print.
Ask AI