← C++, End to End

C++, End to End

Fundamental Data Types

C++ gives you a small set of built-in ("fundamental") types, and the single most important fact about most of them is that the standard does not fix their exact size. It fixes only a minimum guarantee.

c++
bool        // true or false
char        // at least 8 bits, holds one character or a small integer
int         // at least 16 bits, in practice always 32 on any platform you'll touch today
float       // typically 32-bit IEEE 754
double      // typically 64-bit IEEE 754, the default choice for floating point
long        // at least 32 bits
long long   // at least 64 bits

"At least" is doing real work in that list. int has been 32 bits on every mainstream desktop and server platform for decades, but the language never promised that, and code that silently assumes a specific size breaks the moment it meets a platform where that assumption doesn't hold. If you need an exact width, say so explicitly.

c++ · live, editable, runnableOpen in Compiler Explorer ↗

<cstdint> gives you std::int8_t through std::int64_t (and the unsigned versions) specifically so you never have to guess. Reach for these anywhere the exact width actually matters (file formats, network protocols, bit-packed structures); reach for plain int everywhere else, since it's the type the compiler and standard library are most optimized to work with.

sizeof, and why sizes aren't guaranteed

sizeof(T) tells you how many bytes an object of type T occupies on the platform you're compiling for, right now, on this compiler. It's a compile-time operator, not a runtime function call, and its result can (and does) differ across platforms for the exact same source code.

c++ · live, editable, runnableOpen in Compiler Explorer ↗

Try it yourself: run that on Compiler Explorer with a few different compilers/targets from the dropdown (try an ARM target alongside the default x86-64 one). sizeof(long) is the one most likely to actually differ: 8 bytes on 64-bit Linux/macOS, 4 bytes on 64-bit Windows. That single inconsistency has broken real cross-platform code.

Signed versus unsigned

A signed integer type can represent negative numbers; an unsigned one cannot, and in exchange gets roughly double the positive range for the same bit width. Unsigned types wrap around on overflow instead of triggering undefined behavior (an actual, well-defined guarantee, unlike signed overflow), which sounds like a safety feature and is actually one of the most common sources of subtle bugs in C++.

c++ · live, editable, runnableOpen in Compiler Explorer ↗

The classic trap: looping backward with an unsigned index.

c++ · live, editable, runnableOpen in Compiler Explorer ↗

This is common enough, and dangerous enough, that the practical rule most experienced C++ programmers land on is: use plain int (signed) for anything that isn't literally a size or an index into something, and be deliberate and careful with unsigned anywhere it does appear.

Integer and floating-point literals

c++
42          // int
42u         // unsigned int
42l         // long
42ll        // long long
42ull       // unsigned long long
0x2A        // hexadecimal, value 42
052         // octal (leading zero), value 42
0b101010    // binary, value 42
1'000'000   // digit separators (C++14+): purely cosmetic, ignored by the compiler

3.14        // double by default
3.14f       // float
3.14L       // long double

That trailing letter (the "literal suffix") isn't decoration, it changes the literal's actual type, and that type participates in overload resolution and implicit conversions just like any variable's type would.

Type conversion basics

An implicit conversion happens automatically whenever a value of one type is used somewhere a different type is expected, no cast required. Some of these are perfectly safe (int to double, since every int value fits in a double). Some lose information silently.

c++ · live, editable, runnableOpen in Compiler Explorer ↗

Compare that with brace initialization from the first chapter: short s{big}; refuses to compile precisely because it's a narrowing conversion. Assignment (s = big;) allows it silently; initialization with braces does not. That asymmetry is a real, deliberate reason to reach for brace initialization everywhere you can.

An explicit conversion (a cast) says "I know this might lose information, do it anyway." Prefer static_cast over the C-style cast (T)value: it only allows conversions that make sense for the types involved, and it's visually greppable, which matters when you're hunting for every place a program deliberately throws away precision.

c++
double d = 3.99;
int i = static_cast<int>(d);  // explicit, same truncation, but visibly intentional

Try it yourself: write a function that takes an int and a double, adds them, and returns the result as an int. Then write it three ways: relying on the implicit conversions, using static_cast explicitly at each step, and using brace initialization for the final int result to see which conversions it lets through versus which it rejects at compile time.