C++'s fundamental types are the built-in building blocks every other type is made from: booleans, characters, the signed and unsigned integer families, and the floating-point types. Each has a fixed size and a fixed range, and knowing those ranges is what separates a program that computes correctly from one that silently produces nonsense on the tenth ingredient.
C++'s fundamental types at a glance
The standard guarantees a minimum width for each type, and your compiler is free to make them wider. On any modern 64-bit desktop or server the layout below is what you will actually get.
bool— one bit of stored value, logically true or false, at least one byte in memory.char,signed char,unsigned char— exactly one byte.charmay be signed or unsigned depending on the platform, which is a famous portability trap.short,int,long,long long— signed integers of increasing width, at least 16, 32, 64 and 64 bits.unsigned short,unsigned int,unsigned long,unsigned long long— the same widths, with no sign bit.float,double,long double— IEEE-754 floating point, normally 4, 8 and 16 bytes.void— the absence of a type. Used as the return type of functions that produce nothing.
Note what is missing: there is no 8-bit or 16-bit integer type in the standard, only char. That gap is why the fixed-width types in <cstdint> were introduced, and why a recipe calculator that counts grams should not use char.
Asking the compiler about the sizes
Rather than memorising a table, ask the machine. sizeof returns the size of a type in bytes, and std::numeric_limits returns the smallest and largest representable values.
#include
#include
#include
int main() {
std::cout << "type bytes min max\n";
std::cout << "-------------------------------------------\n";
std::cout << "bool " << sizeof(bool) << " "
<< std::numeric_limits::min() << " "
<< std::numeric_limits::max() << "\n";
std::cout << "int " << sizeof(int) << " "
<< std::numeric_limits::min() << " "
<< std::numeric_limits::max() << "\n";
std::cout << "unsigned int " << sizeof(unsigned int) << " 0 "
<< std::numeric_limits::max() << "\n";
std::cout << "long long " << sizeof(long long) << " "
<< std::numeric_limits::min() << " "
<< std::numeric_limits::max() << "\n";
std::cout << "double " << sizeof(double) << " "
<< std::numeric_limits::lowest() << " "
<< std::numeric_limits::max() << "\n";
return 0;
}On a typical 64-bit Linux machine that prints:
type bytes min max
-------------------------------------------
bool 1 0 1
int 4 -2147483648 2147483647
unsigned int 4 0 4294967295
long long 8 -9223372036854775808 9223372036854775807
double 8 -1.79769e+308 1.79769e+308Two observations matter for daily work. First, int is 32 bits on almost every platform, so it tops out at 2,147,483,647. Second, double has a huge range but only about 15 to 17 significant decimal digits, so it can represent 1234567890123456789.0 approximately, not exactly.
Signed overflow wraps, and that is a trap
Signed integer overflow is in C++. The compiler is allowed to assume it never happens and may optimise accordingly, so the result you get is not guaranteed. Unsigned overflow, by contrast, is defined: the value wraps around modulo the maximum plus one.undefined behaviour
#include
int main() {
unsigned int servings = 0;
servings = servings - 1; // deliberate underflow
std::cout << "unsigned underflow: " << servings << "\n";
int counter = 2147483647;
counter = counter + 1; // signed overflow: undefined behaviour
std::cout << "signed overflow: " << counter << "\n";
return 0;
}The unsigned case is predictable and wraps to the largest value. The signed case is undefined, and in practice with optimisation enabled it frequently produces the mathematically expected answer — but "frequently" is not a specification. Never rely on signed overflow, and never let it happen by accident.
unsigned underflow: 4294967295
signed overflow: -2147483648The second line shows what this particular build produced, wrapping back to the most negative value. Change the compiler, the optimisation level or the surrounding code and you may get something else entirely — which is exactly the problem with .undefined behaviour
Floating point is not exact either
Binary floating point cannot represent most decimal fractions exactly, just as it cannot represent one tenth exactly. This is why 0.1 plus 0.2 is famously not equal to 0.3.
#include
int main() {
double a = 0.1;
double b = 0.2;
std::cout << "0.1 + 0.2 == 0.3 : " << std::boolalpha << (a + b == 0.3) << "\n";
std::cout << "0.1 + 0.2 : " << (a + b) << "\n";
std::cout << "exactly 0.3 : " << 0.3 << "\n";
std::cout << "rounded to 2 dp : " << (a + b) * 100.0 / 100.0 << "\n";
return 0;
}The output shows the tiny difference and the surprising false comparison:
0.1 + 0.2 == 0.3 : false
0.1 + 0.2 : 0.3
exactly 0.3 : 0.3
rounded to 2 dp : 0.3Note the second line: the default output precision hides the difference, so the value prints as 0.3 and looks perfectly correct. The comparison on the first line still says false, because the underlying bits differ even though the printed text agrees.
For money, never use double. Use integer counts of cents, or a dedicated decimal type. For scientific measurements, double is the right default and float is the compact option.
Fixed-width integers and size_t
When a value crosses a file boundary or a network protocol, its width must be exact, not merely "at least". The types in <cstdint> give you that guarantee: int8_t, int16_t, int32_t, int64_t and their unsigned counterparts, plus intmax_t and uintmax_t.
For sizes and counts, the standard library provides std::size_t, an unsigned type wide enough to represent the size of any object. It is what sizeof returns and what container sizes use.
#include
#include
int main() {
std::int32_t cups = 4;
std::uint64_t total_millilitres = 1500ULL;
std::size_t ingredient_count = 7;
std::cout << "cups: " << cups << "\n";
std::cout << "total millilitres: " << total_millilitres << "\n";
std::cout << "ingredient count: " << ingredient_count << "\n";
return 0;
}The ULL suffix on the literal is worth noting: an unsuffixed large integer literal is a long or long long depending on its value, and the suffix makes the unsigned 64-bit intent explicit.
Choosing a type for real data
A short decision table covers most cases.
- Counts of items, ages, small integers —
int. It is 32 bits and never surprises you. - Very large counts or values that cannot be negative —
unsigned intorunsigned long long. - Weights, temperatures, prices in dollars —
double. - Whole cents, bytes, exact decimals —
long longorstd::int64_t. - A value that must round-trip through a file or network —
std::int32_tand friends. - A placeholder that holds anything — avoid
void*in application code; it throws away type safety.
Quick reference
sizeofandstd::numeric_limitstell you the real sizes and ranges on your machine.intis 32-bit almost everywhere;long longis at least 64-bit.- Unsigned overflow wraps and is defined; signed overflow is
.undefinedbehaviour doubleholds about 15 to 17 significant decimal digits and cannot represent 0.1 exactly.std::int32_tand friends give exact widths for data that crosses a boundary.std::size_tis the right type for sizes, counts of elements and buffer lengths.
Try it yourself: write a program that prints the size and range of every fundamental type on your machine, then repeat it on a different machine or a 32-bit container and compare. Next, add a recipe ingredient whose quantity is stored in grams and multiply it by a serving factor, watching what happens when the result overflows a 16-bit [[int]].