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
#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:
#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:
- Use
intfor counting things unless you have a reason not to. It is the type the language and the hardware are tuned for. - Use
double, notfloat, unless you have measured that you need the memory.floathas about 7 significant digits, which runs out sooner than people expect. - Use
std::size_tfor sizes and indices. It is what the standard library uses, and mixing it withintproduces the comparison warning in the next section.
Integer division truncates
This is the single most common surprise in the language’s arithmetic:
#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:
#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 result — static_cast<double>(total / count)
— would be too late: the truncation has already happened.
Signed, unsigned, and the comparison you should not write
#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:
#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
#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
==:
#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.
#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.