The C++ Textbook

Part 1 · Foundations

Values, types, and names

Why C++ insists on knowing the type of everything, and what that buys you.

By the end of this chapter you can

  • Choose an appropriate built-in type for a quantity
  • Predict the result of integer versus floating-point division
  • Explain what auto deduces and when to avoid it

Every value in a C++ program has a type, fixed when you compile and never changing while the program runs. This is unlike Python or JavaScript, where a name can hold a number now and a string later, and it is the reason C++ programs can be fast: if the compiler knows a thing is a 32-bit integer, it can emit one instruction to add it instead of a function call that inspects it first.

The cost is that you have to say what you mean. This chapter is about saying it well.

Declaring a variable

Four declarations
#include <iostream>

int main() {
    int    apples      = 5;
    double temperature = 21.5;
    bool   is_ready    = true;
    char   grade       = 'A';

    std::cout << apples << ' ' << temperature << ' '
              << is_ready << ' ' << grade << '\n';
}

Read each line as type, then name, then initial value. The type is not a hint or an annotation the compiler might ignore; it decides how many bytes the object occupies, what operations are legal on it, and what the machine instructions will be.

Notice is_ready printed as 1, not true. std::cout prints a bool as an integer unless you ask otherwise with std::boolalpha. That is a small example of a large idea: the type controls what happens, even at the point of printing.

Prove it to yourself. This program is undefined, and what it does about that is worth watching:

Do not do this
#include <iostream>

int main() {
    int uninitialised;                    // no value
    std::cout << uninitialised << '\n';   // reading it is undefined behaviour
}

Three things happened. It compiled — nothing here is illegal in the sense the compiler must reject. It warned, because -Wall includes -Wuninitialized and the compiler could see the mistake from the source alone. And it printed a number that means nothing: whatever bytes happened to be at that address. Run it again and you may get something different.

The built-in types worth knowing

Type Typical size Use it for
bool 1 byte true or false
char 1 byte one byte of text
int 4 bytes general-purpose whole numbers
long long 8 bytes whole numbers beyond ±2 billion
std::size_t 8 bytes sizes and indices, never negative
float 4 bytes real numbers where 7 digits is enough
double 8 bytes real numbers, the default choice

Three rules cover most decisions:

  1. Use int for counting things unless you have a reason not to. It is the type the language and the hardware are tuned for.
  2. Use double, not float, unless you have measured that you need the memory. float has about 7 significant digits, which runs out sooner than people expect.
  3. Use std::size_t for sizes and indices. It is what the standard library uses, and mixing it with int produces the comparison warning in the next section.

Integer division truncates

This is the single most common surprise in the language’s arithmetic:

Two divisions that look the same
#include <iostream>

int main() {
    int    a = 7,   b = 2;
    double x = 7.0, y = 2.0;

    std::cout << "7 / 2     = " << a / b << '\n';
    std::cout << "7.0 / 2.0 = " << x / y << '\n';
    std::cout << "7 / 2.0   = " << a / y << '\n';
    std::cout << "7 % 2     = " << a % b << "   (the remainder)\n";
}

When both operands are integers, / is integer division: it discards the fractional part rather than rounding. 7 / 2 is 3, not 3.5 and not 4. If either operand is a floating-point type, the other is converted first and you get real division.

This bites hardest when computing an average:

An average that is wrong, and one that is right
#include <iostream>

int main() {
    int total = 7;
    int count = 2;

    std::cout << "wrong: " << total / count << '\n';
    std::cout << "right: " << static_cast<double>(total) / count << '\n';
}

static_cast<double>(total) converts before the division, so the division is a floating-point one. Casting the resultstatic_cast<double>(total / count) — would be too late: the truncation has already happened.

Signed, unsigned, and the comparison you should not write

A loop that never ends
#include <iostream>
#include <vector>

int main() {
    std::vector<int> v{10, 20, 30};

    // v.size() is unsigned. Subtracting 1 from an unsigned 0 does not give -1.
    std::cout << "v.size() - 1 = " << v.size() - 1 << '\n';

    std::vector<int> empty;
    std::cout << "empty.size() - 1 = " << empty.size() - 1 << '\n';
}

empty.size() is 0 as a std::size_t. Subtracting 1 wraps to the largest representable value — about 18 quintillion. A loop written for (std::size_t i = 0; i <= v.size() - 1; ++i) over an empty vector will therefore run essentially forever, reading far past the end.

Prefer a range-based loop, which cannot get this wrong:

The loop that cannot go wrong
#include <iostream>
#include <vector>

int main() {
    std::vector<int> v{10, 20, 30};
    for (int value : v) {
        std::cout << value << ' ';
    }
    std::cout << '\n';

    std::vector<int> empty;
    for (int value : empty) {
        std::cout << "this never runs " << value;
    }
    std::cout << "(nothing above)\n";
}

Floating point is not the reals

A third is not a third
#include <iostream>
#include <iomanip>

int main() {
    double a = 0.1, b = 0.2;

    std::cout << std::setprecision(20);
    std::cout << "0.1 + 0.2 = " << a + b << '\n';
    std::cout << "equal to 0.3? " << ((a + b) == 0.3 ? "yes" : "no") << '\n';
}

double stores values in binary, and one tenth has no exact binary representation, exactly as one third has no exact decimal one. The stored value is very slightly off, the errors accumulate, and == reports the truth: these are different numbers.

The fix is to compare with a tolerance appropriate to your problem, not to use ==:

Comparing with a tolerance
#include <iostream>
#include <cmath>

bool close_enough(double a, double b, double tolerance = 1e-9) {
    return std::fabs(a - b) <= tolerance;
}

int main() {
    std::cout << std::boolalpha;
    std::cout << "0.1 + 0.2 == 0.3          : " << (0.1 + 0.2 == 0.3) << '\n';
    std::cout << "close_enough(0.1+0.2, 0.3): " << close_enough(0.1 + 0.2, 0.3) << '\n';
}

auto, and when it helps

auto asks the compiler to deduce the type from the initialiser. The variable is still statically typed — you have just declined to write the type out.

auto in its element
#include <iostream>
#include <map>
#include <string>

int main() {
    std::map<std::string, int> scores{{"ada", 100}, {"alan", 92}};

    // Without auto: std::map<std::string, int>::const_iterator it = scores.find("ada");
    auto it = scores.find("ada");
    if (it != scores.end()) {
        std::cout << it->first << " scored " << it->second << '\n';
    }

    for (const auto& [name, score] : scores) {
        std::cout << name << ": " << score << '\n';
    }
}

Use auto when the type is long, obvious from the right-hand side, or genuinely unspeakable (a lambda’s type has no name you can write). Avoid it when the type is the point — auto x = 0; tells a reader nothing that int x = 0; does not, and auto count = v.size(); hides that the type is unsigned, which is the detail that matters.

Check yourself

Practice