Fundamental Types
In managed languages (Java, C#), types are abstract constraints enforced by a virtual machine. In C++, types are direct mappings to hardware capabilities. A uint64_t translates directly to a 64-bit register; a float maps directly to an FPU instruction.
This section analyzes how C++ types map to physical memory, the fragmentation caused by operating System data models, and the standard mechanisms for handling byte ordering (Endianness).
The Data Models (LP64 vs. LLP64)
The C++ Standard does not define exact bit-widths for types like int or long. It only defines relative minimum ranges.
short: At least 16 bits.int: At least 16 bits ( 32).long: At least 32 bits.long long: At least 64 bits.
Because of this flexibility, operating systems evolved different Data Models. This creates the Primary portability hazard in C++ systems programming.
Data Model Comparison
| Type | ILP32 (32-bit Systems) | LLP64 (Windows x64) | LP64 (Linux/macOS x64) |
|---|---|---|---|
short | 16 | 16 | 16 |
int | 32 | 32 | 32 |
long | 32 | 32 | 64 |
long long | 64 | 64 | 64 |
void* | 32 | 64 | 64 |
size_t | 32 | 64 | 64 |
The long Trap
The most dangerous divergence is long.
- On Linux,
longis 64-bit. - On Windows,
longis 32-bit. - Consequence: Never use
longfor pointer arithmetic or file offsets. It will truncate pointers on Windows and overflow file sizes > 2GB.
Architectural Solution: Fixed-Width Integers (<cstdint>)
For binary layouts, network protocols, or file formats, never use fundamental types (int short, long). Use fixed-width aliases.
int32_t, uint32_t: Exact 32-bit width (Two’s complement).int64_t, uint64_t: Exact 64-bit width.intptr_t: An integer large enough to hold a pointer (matchesvoid*width).
Special Systems Types
C++ defines specific types for memory interaction that communicate intent to both the compiler and the programmer.
1. std::byte (C++17)
Historically, char or unsigned char was used to access raw memory. This was semantically Ambiguous (is it text or data?).
- Definition:
enum class byte : unsigned char {}; - Behavior: It is not an arithmetic type. You cannot do
byte + 1. You can only perform bitwise operations (|, &, ^, <<, >>). - Usage: Strictly for raw memory buffers and binary I/O.
2. size_t
- Definition: Unsigned integer capable of holding the size of the largest possible object.
- Hardware Mapping: Matches the native bus width (32-bit or 64-bit).
- Usage: Array indexing, loop counters, and memory sizes.
3. ptrdiff_t
- Definition: Signed integer result of subtracting two pointers pointing to the same array.
- Hardware Mapping: Matches the pointer width.
- Usage: Pointer arithmetic and iterator distances.
4. void
- Usage: Incomplete type.
void*represents a raw memory address with no type info. - Arithmetic:
void* + 1is illegal in ISO C++ (though GCC allows it as an extension treating it likechar*). Always disable this extension (-Wpointer-arith) to ensure portability.
Hardware Representation
Two’s Complement (C++20)
Prior to C++20, the standard allowed hardware to use Sign-Magnitude or Ones’ Complement. C++20 Mandates Two’s Complement representation for signed integers.
This standardizes the behavior of bitwise operations on signed integers.
-1is represented as all ones (0xFF...FF).- Bit shifting signed negative numbers is now well-defined arithmetic shifting.
Floating Point (IEEE 754)
While C++ does not strictly mandate IEEE 754, practically all supported hardware uses it.
float: IEEE 754 binary32 (1 sign, 8 exponent, 23 mantissa).double: IEEE 754 binary64 (1 sign, 11 exponent, 52 mantissa).- C++23 Extended Types:
<stdfloat>introducesstd::float16_t, std::float128_tAndstd::bfloat16_t(Brain Floating Point), enabling interoperability with AI accelerators and GPU storage formats.
Endianness
Endianness describes the order in which bytes of a multi-byte word are stored in memory.
- Little Endian (LE): Least Significant Byte (LSB) at the lowest address.
- Hardware: x86, x86_64, modern ARM (Android/iOS/macOS).
- Representation of
0x12345678:78 56 34 12.
- Big Endian (BE): Most Significant Byte (MSB) at the lowest address.
- Hardware: Mainframes (z/Architecture), Legacy PowerPC, Network Protocols (TCP/IP).
- Representation of
0x12345678:12 34 56 78.
Detecting Endianness (C++20)
Do not use legacy pointer casting tricks (int x = 1; if (*(char*)&x)...). These are technically Type Punning (Module 8.3) and can fail at compile time.
C++20 introduces std::endian in <bit>.
#include <bit>
void check_endianness() \{
if constexpr (std::endian::native == std::endian::little) \{
// Build path for Intel/ARM
\} else if constexpr (std::endian::native == std::endian::big) \{
// Build path for Network/Mainframe
\} else \{
// Mixed endian (exotic hardware like PDP-11)
\}
\}
To allow this godbolt iframe to showcase the endianness between x86_64 gcc and powerpc gcc, right Click on the print and select reveal linked code. This will direct you to the correct location in Asm.
Handling Endianness (C++23)
When parsing binary formats (file headers) or network packets, you must enforce a specific Endianness regardless of the host CPU.
Legacy: Manual bit-shifting ((b0 << 24) | (b1 << 16)...). Modern C++23: Use std::byteswap.
#include <bit>
#include <cstdint>
uint32_t parse_network_packet(uint32_t network_val) \{
if constexpr (std::endian::native == std::endian::little) \{
// Convert Big Endian (Network) to Little Endian (Host)
return std::byteswap(network_val);
\} else \{
// Host is already Big Endian
return network_val;
\}
\}
std::byteswap compiles down to a single CPU instruction:
- x86_64:
BSWAP - ARM64:
REV
Integer Arithmetic Safety
Mapping mathematical integers to fixed-width hardware registers introduces overflow risks.
Signed Integer Overflow
Behavior: Undefined Behavior (UB). The compiler assumes signed overflow never happens. This allows optimizations like if (x + 1 > x) true.
- If
xisINT_MAX, x + 1overflows. If the hardware wraps, the check fails. If the compiler optimized the check away, security vulnerabilities occur.
Unsigned Integer Overflow
Behavior: Defined (Modulo Arithmetic). Unsigned integers wrap around (UINT_MAX + 1 == 0). This makes unsigned useful for bitmasks and modular arithmetic, but dangerous for loops like for (unsigned i = 5; i >= 0; --i).
Architectural Mitigation
For safe arithmetic, use C++20 standard functions in <utility> which detect overflow without UB.
#include <utility>
#include <limits>
int safe_add(int a, int b) \{
int result;
if (std::cmp_greater(a, std::numeric_limits<int>::max() - b)) \{
// Handle Error
\}
return a + b;
\}
Floating-Point Representation in Depth
IEEE 754 Binary Layout
An IEEE 754 floating-point number encodes a signed value using three fields:
[sign (1 bit)] [exponent (e bits)] [mantissa/fraction (m bits)]
The encoded value is:
the implicit leading 1. (the “hidden bit”) is the key optimization of IEEE 754: since the mantissa is normalized so the leading digit is always 1, it need not be stored.
| Type | Total Bits | Sign | Exponent | Mantissa | Bias | Exponent Range |
|---|---|---|---|---|---|---|
float | 32 | 1 | 8 | 23 | 127 | -126 to +127 |
double | 64 | 1 | 11 | 52 | 1023 | -1022 to +1023 |
Special Values
IEEE 754 reserves specific exponent values for special cases:
| Exponent Field | Mantissa Field | Meaning |
|---|---|---|
| All zeros | All zeros | Signed zero (±0) |
| All zeros | Non-zero | Denormalized (subnormal) number |
| All ones | All zeros | Infinity (±inf) |
| All ones | Non-zero | NaN (Not a Number) |
Denormalized numbers allow gradual underflow: as values approach zero, precision decreases Linearly rather than dropping abruptly to zero. Without denormals, the gap between the smallest Normalized number and zero would be huge.
#include <iostream>
#include <limits>
#include <cmath>
int main() \{
float smallest_normal = std::numeric_limits<float>::min(); // ~1.175e-38
float smallest_denorm = std::numeric_limits<float>::denorm_min(); // ~1.401e-45
std::cout << "Smallest normal: " << smallest_normal << "\n";
std::cout << "Smallest denorm: " << smallest_denorm << "\n";
std::cout << "Has denormals: " << std::numeric_limits<float>::has_denorm << "\n";
// Denormals can cause severe performance penalties on some architectures
// (up to 100x slower than normal FP ops on some x86 CPUs)
// Consider enabling FTZ (Flush-To-Zero) for performance-critical code
\}
NaN Propagation
NaN is the only floating-point value that does not compare equal to itself:
#include <cmath>
#include <iostream>
int main() \{
double nan_val = std::nan("");
std::cout << "nan == nan: " << (nan_val == nan_val) << "\n"; // 0 (false)
std::cout << "nan != nan: " << (nan_val != nan_val) << "\n"; // 1 (true)
// This is the standard way to test for NaN:
std::cout << "isnan: " << std::isnan(nan_val) << "\n"; // 1 (true)
// NaN propagates through arithmetic:
double result = nan_val + 1.0; // result is NaN
std::cout << "nan + 1.0 = " << result << "\n"; // nan
\}
-ffast-math and Its Dangers
Compilers offer a -ffast-math flag (GCC/Clang) that enables aggressive floating-point Optimizations. This flag relaxes IEEE 754 compliance:
| Optimization | Effect |
|---|---|
Associative FP (-fassociative-math) | Reorders a + (b + c) to (a + b) + c (changes rounding) |
| Reciprocal math | Replaces x / y with x * (1/y) |
| Finite math only | Assumes no NaN/Inf (removes NaN checks) |
| No signed zeros | Assumes +0 == -0 |
# WARNING: -ffast-math can change numerical results and break NaN/Inf handling
clang++ -O3 -ffast-math app.cpp -o app
Never use -ffast-math for: scientific computing, financial calculations, any code that relies On NaN propagation for error detection, or code that compares floating-point values for equality
Alignment and Padding
Natural Alignment
Every fundamental type has an alignment requirement: the address at which it must be stored must be a multiple of a specific power of two. On x86_64:
| Type | Size (bytes) | Alignment (bytes) |
|---|---|---|
char | 1 | 1 |
short | 2 | 2 |
int | 4 | 4 |
long | 8 (LP64) | 8 |
long long | 8 | 8 |
float | 4 | 4 |
double | 8 | 8 |
pointer | 8 | 8 |
Misaligned access on x86 works but is slower (splits into multiple memory transactions). On ARM and RISC-V, misaligned access can cause a hardware exception (SIGBUS).
Struct Padding
The compiler inserts padding bytes between struct members to satisfy alignment requirements:
#include <cstddef>
#include <iostream>
struct Natural \{
char a; // offset 0, size 1
// 3 bytes padding
int b; // offset 4, size 4 (aligned to 4)
short c; // offset 8, size 2
// 2 bytes padding
double d; // offset 16, size 8 (aligned to 8)
\}; // Total size: 24 bytes
struct Packed \{
char a; // offset 0
int b; // offset 4
short c; // offset 8
double d; // offset 16
\};
int main() \{
std::cout << "sizeof(Natural) = " << sizeof(Natural) << "\n"; // 24
std::cout << "sizeof(Packed) = " << sizeof(Packed) << "\n"; // 24
// Both have the same layout because padding is unavoidable for alignment
// Offset demonstration
std::cout << "offsetof a: " << offsetof(Natural, a) << "\n"; // 0
std::cout << "offsetof b: " << offsetof(Natural, b) << "\n"; // 4
std::cout << "offsetof c: " << offsetof(Natural, c) << "\n"; // 8
std::cout << "offsetof d: " << offsetof(Natural, d) << "\n"; // 16
\}
Controlling Alignment: alignas and alignof
C++11 introduced alignas to specify custom alignment and alignof to query it:
#include <iostream>
struct alignas(64) CacheAligned \{
int data[16];
\};
struct alignas(1) PackedStruct \{
double d; // Potentially misaligned!
\};
int main() \{
std::cout << "alignof(CacheAligned) = " << alignof(CacheAligned) << "\n"; // 64
std::cout << "sizeof(CacheAligned) = " << sizeof(CacheAligned) << "\n"; // 64 (padded to alignment)
// 64-byte alignment is useful for SIMD (AVX-512 vectors are 512 bits = 64 bytes)
// and for avoiding false sharing in multi-threaded code
alignas(16) int arr[4]; // Aligned to 16-byte boundary
// Useful for SSE loads/stores (128-bit vectors require 16-byte alignment)
\}
#pragma pack and __attribute__((packed))
To eliminate padding (e.g., for network protocols or file formats), use packing:
#pragma pack(push, 1) // Set alignment to 1 byte
struct NetworkPacket \{
uint8_t type;
uint32_t length;
uint16_t checksum;
\};
#pragma pack(pop)
static_assert(sizeof(NetworkPacket) == 7); // No padding: 1 + 4 + 2 = 7
// GCC/Clang alternative:
struct __attribute__((packed)) NetworkPacket \{
uint8_t type;
uint32_t length;
uint16_t checksum;
\};
Warning: Accessing a misaligned member on strict-alignment architectures (ARM, RISC-V) causes Undefined behavior or a hardware trap. Use std::memcpy to safely extract packed data:
#include <cstring>
#include <cstdint>
struct __attribute__((packed)) PackedU32 \{
uint8_t bytes[4];
\};
uint32_t safe_read(const PackedU32& p) \{
uint32_t result;
std::memcpy(&result, p.bytes, sizeof(result)); // Safe: compiler generates correct loads
return result;
\}
C++23 Extended Floating-Point Types
The <stdfloat> header (C++23) introduces fixed-width floating-point types that map to hardware or Software implementations:
#include <stdfloat>
#include <iostream>
int main() \{
std::float16_t half = 3.14f16; // IEEE 754 binary16 (half precision)
std::bfloat16_t bfloat = 3.14bf16; // Brain Float 16 (8 exponent, 7 mantissa)
std::float32_t single = 3.14f32; // IEEE 754 binary32
std::float64_t dbl = 3.14f64; // IEEE 754 binary64
std::float128_t quad = 3.14f128; // IEEE 754 binary128 (software on most platforms)
\}
Brain Float 16 (bfloat16) is particularly important for machine learning inference. It has the Same exponent range as float32 (8 bits) but only 7 bits of mantissa, providing 3 decimal digits of Precision. The reduced mantissa halves memory bandwidth compared to float32 while maintaining the Same dynamic range, preventing overflow/underflow in deep learning workloads
Boolean Type and Integer Promotion
bool and Integer Conversion
In C++, bool is a fundamental type that can hold values true (1) or false (0). When a bool Participates in arithmetic, it is promoted to int:
#include <iostream>
int main() \{
bool flag = true;
int result = flag + 1; // result == 2 (bool promoted to int(1))
std::cout << result << "\n";
// Dangerous: any non-zero value converts to true
int x = 42;
bool b = x; // b == true
std::cout << std::boolalpha << b << "\n"; // true
// Integral to bool: 0 → false, anything else → true
std::cout << std::boolalpha << static_cast<bool>(0) << "\n"; // false
std::cout << std::boolalpha << static_cast<bool>(-1) << "\n"; // true
std::cout << std::boolalpha << static_cast<bool>(INT_MAX) << "\n"; // true
\}
Integer Promotion Rules
The C++ standard defines specific integral promotion rules [N4950 §7.3.7]:
bool→intchar, signed char, unsigned char, char8_t→int(ifintcan represent all values)short→int(ifintcan represent all values)wchar_t, char16_t, char32_t, char8_t→ the first ofint, unsigned int, longunsigned longthat can represent all values these promotions occur before arithmetic operations and can cause surprising results:
#include <iostream>
#include <cstdint>
int main() \{
uint16_t a = 65535;
uint16_t b = 1;
auto c = a + b; // Both promoted to int (65535 + 1 = 65536)
// c has type int, value 65536, no overflow!
std::cout << c << "\n"; // 65536
// But if you store back to uint16_t:
uint16_t d = a + b; // 65536 truncated to uint16_t → 0
std::cout << d << "\n"; // 0
\}
Intuition
Basic building blocks: Fundamental types are like atoms, they’re the simplest data types from which all other types are built. Their size and behavior vary by platform.
Why it matters: Understanding fundamental types helps you write portable code and avoid platform-specific bugs.
The key insight: int is at least 16 bits, but in standard practice 32 bits, never assume the size of fundamental types without checking.
Common Pitfalls
- Using
intorlongfor binary protocol fields. The size ofintandlongvaries across platforms. Always use<cstdint>fixed-width types (uint32_t, int64_t. Etc.) for network protocols, file formats, and shared memory structures. - Signed integer overflow UB. The compiler assumes signed overflow never happens and optimizes based on this assumption. Use
-fsanitize=signed-integer-overflowduring testing to catch these at runtime. For production code, usestd::cmp_less, std::cmp_greaterAndstd::in_range<T>from<utility>(C++20). - Comparing floating-point values with
==. Due to rounding errors, two floating-point computations that should produce the same result may differ by tiny amounts. Use an epsilon comparison orstd::nextafterfor approximate equality tests. However,==is valid for exact comparisons (e.g., checking against zero after a known computation). - Assuming
sizeof(long) == sizeof(void*). This is true on LP64 (Linux/macOS) but false on LLP64 (Windows), wherelongis 32 bits butvoid*is 64 bits. Useintptr_toruintptr_tfor pointer-sized integers. - Packed struct member access on ARM/RISC-V.
__attribute__((packed))can generate misaligned loads/stores. On x86, this is merely slow; on ARM, it can crash. Usestd::memcpyto safely extract values from packed structs. - Denormal performance. Denormalized floating-point numbers can cause 10-100x slowdowns on x86 CPUs. In performance-critical code (audio processing, game physics), consider enabling Flush-To-Zero (
_MM_DENORMALS_ZERO_ON) via compiler flags (-ffast-mathor-fno-denormals) if exact gradual underflow is not required.
Summary
This topic covers the essential concepts and techniques related to fundamental types, including key principles and practical applications. Key concepts include:
- core concepts and definitions
- key principles and frameworks
- practical applications
- common techniques and methods
- evaluation and critical analysis A thorough understanding of these concepts, combined with regular practice and review, is essential for mastery of this topic.