The C++ Textbook

Part 1 · Foundations

Making decisions

if, else, switch, and the boolean logic underneath them.

By the end of this chapter you can

  • Write conditions that short-circuit correctly
  • Use switch without falling through by accident
  • Explain why comparing floating-point numbers with == is a trap

A program that always does the same thing is a calculation. A program that chooses is where the work starts.

if, and the shape of a condition

Choosing between two paths
#include <iostream>

int main() {
    const int temperature = 12;

    if (temperature < 0) {
        std::cout << "freezing\n";
    } else if (temperature < 15) {
        std::cout << "cold\n";
    } else {
        std::cout << "warm\n";
    }
}

The condition must be something convertible to bool. Comparisons produce one directly: <, <=, >, >=, ==, !=.

Braces are optional for a single statement and you should use them anyway:

Why the braces are not optional in practice
#include <iostream>

int main() {
    const bool ready = false;

    // Only the first line is conditional. The second always runs.
    if (ready)
        std::cout << "starting\n";
        std::cout << "  this looks conditional and is not\n";

    std::cout << "\nwith braces, the grouping is what it looks like\n";
    if (ready) {
        std::cout << "starting\n";
        std::cout << "this really is conditional\n";
    }
}

The indentation lies in the first case. Braces cost two characters and remove a whole category of edit-time mistake.

Declaring inside the condition

C++17 lets you declare a variable in the if itself, scoping it to the branches:

if with an initialiser
#include <iostream>
#include <map>
#include <string>

int main() {
    const std::map<std::string, int> ages{{"ada", 36}, {"alan", 41}};

    if (auto it = ages.find("ada"); it != ages.end()) {
        std::cout << it->first << " is " << it->second << '\n';
    }
    // `it` does not exist here, which is exactly right.

    if (auto it = ages.find("nobody"); it != ages.end()) {
        std::cout << "found\n";
    } else {
        std::cout << "no entry for nobody\n";
    }
}

Use it whenever the variable is only meaningful inside the branch. A name that cannot leak cannot be misused later.

Boolean logic, and short-circuiting

&& is and, || is or, ! is not. The important property is that && and || short-circuit: they evaluate the right side only if they must.

The right side may never run
#include <iostream>
#include <vector>

bool noisy(const char* label, bool value) {
    std::cout << "  evaluating " << label << '\n';
    return value;
}

int main() {
    std::cout << "false && ...\n";
    if (noisy("left", false) && noisy("right", true)) {}

    std::cout << "true || ...\n";
    if (noisy("left", true) || noisy("right", true)) {}

    std::cout << "\nshort-circuiting is what makes this safe:\n";
    std::vector<int> v;
    if (!v.empty() && v[0] > 0) {          // v[0] never runs on an empty vector
        std::cout << "first is positive\n";
    } else {
        std::cout << "empty, and we never touched v[0]\n";
    }
}

That last pattern — check that something exists, then look at it — depends entirely on short-circuiting. Reverse the operands and the program reads out of bounds before discovering there was nothing there.

Comparing floating-point numbers

Chapter 1.2 showed that 0.1 + 0.2 != 0.3. That has a direct consequence for conditions:

A condition that is never true
#include <cmath>
#include <iostream>

int main() {
    const double sum = 0.1 + 0.2;

    if (sum == 0.3) {
        std::cout << "equal\n";
    } else {
        std::cout << "not equal — and this is the branch that runs\n";
    }

    // Compare with a tolerance appropriate to the problem.
    if (std::fabs(sum - 0.3) < 1e-9) {
        std::cout << "close enough, which is the question you meant to ask\n";
    }
}

< and > on doubles are fine. It is == and != that are almost always the wrong question — and != is worse, because a loop written while (x != target) may never terminate.

switch

When you are comparing one value against many constants, switch says so more clearly than a chain of else if:

switch over an enum
#include <iostream>
#include <string_view>

enum class Direction { north, east, south, west };

std::string_view describe(Direction d) {
    switch (d) {
        case Direction::north: return "up";
        case Direction::east:  return "right";
        case Direction::south: return "down";
        case Direction::west:  return "left";
    }
    return "unknown";
}

int main() {
    for (Direction d : {Direction::north, Direction::east,
                        Direction::south, Direction::west}) {
        std::cout << describe(d) << ' ';
    }
    std::cout << '\n';
}

Switching over an enum class and handling every enumerator has a real benefit: add a fifth direction and the compiler warns that the switch no longer covers every case. A chain of else if with a final else would silently take the fallback instead.

Fallthrough

A case without break continues into the next one. Sometimes that is what you want; usually it is a bug:

Fallthrough, accidental and deliberate
#include <iostream>

void classify(int score) {
    std::cout << score << ": ";
    switch (score) {
        case 0:
            std::cout << "none ";
            [[fallthrough]];          // deliberate, and says so
        case 1:
        case 2:
            std::cout << "low\n";
            break;
        case 3:
            std::cout << "medium\n";
            break;
        default:
            std::cout << "high\n";
            break;
    }
}

int main() {
    for (int score : {0, 1, 2, 3, 9}) classify(score);
}

Two things there. Stacked labels (case 1: case 2:) share a body and are not fallthrough — that is the normal way to group values. And [[fallthrough]]; marks a deliberate fall from one body into the next; without it, compilers with -Wimplicit-fallthrough warn, which is exactly the warning you want.

A declaration the switch cannot allow
int main() {
    int value = 1;
    switch (value) {
        case 1:
            int result = 10;      // error: jump to case label crosses this
            return result;
        case 2:
            return 0;
    }
}

The conditional operator

For choosing between two values rather than two actions, ?: is compact and clear:

Choosing a value
#include <iostream>
#include <string>

int main() {
    const int count = 1;

    // The conditional operator is an expression, so it can initialise a const.
    const std::string word = count == 1 ? "item" : "items";
    std::cout << count << ' ' << word << '\n';

    const int a = 7, b = 3;
    std::cout << "larger: " << (a > b ? a : b) << '\n';
}

It earns its place when both branches produce a value of the same type and the whole thing fits on one line. Nested conditionals — a ? b : c ? d : e — are where it stops being clearer than an if.

Check yourself

Practice