Skip to content

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

TypeILP32 (32-bit Systems)LLP64 (Windows x64)LP64 (Linux/macOS x64)
short161616
int323232
long323264
long long646464
void*326464
size_t326464

The long Trap

The most dangerous divergence is long.

  • On Linux, long is 64-bit.
  • On Windows, long is 32-bit.
  • Consequence: Never use long for 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 (matches void* 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* + 1 is illegal in ISO C++ (though GCC allows it as an extension treating it like char*). 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.

  • -1 is 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> introduces std::float16_t, std::float128_tAnd std::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.

  1. 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.
  1. 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) →\to true.

  • If x is INT_MAX, x + 1 overflows. 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:

value=(−1)sign×2(exponent−bias)×(1.mantissa)\mathrm{value} = (-1)^{\mathrm{sign} \times 2^{(\mathrm{exponent}}} - \mathrm{bias)} \times (1.\mathrm{mantissa)}

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.

TypeTotal BitsSignExponentMantissaBiasExponent Range
float321823127-126 to +127
double64111521023-1022 to +1023

Special Values

IEEE 754 reserves specific exponent values for special cases:

Exponent FieldMantissa FieldMeaning
All zerosAll zerosSigned zero (±0)
All zerosNon-zeroDenormalized (subnormal) number
All onesAll zerosInfinity (±inf)
All onesNon-zeroNaN (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:

OptimizationEffect
Associative FP (-fassociative-math)Reorders a + (b + c) to (a + b) + c (changes rounding)
Reciprocal mathReplaces x / y with x * (1/y)
Finite math onlyAssumes no NaN/Inf (removes NaN checks)
No signed zerosAssumes +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:

TypeSize (bytes)Alignment (bytes)
char11
short22
int44
long8 (LP64)8
long long88
float44
double88
pointer88

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 → int
  • char, signed char, unsigned char, char8_t → int (if int can represent all values)
  • short → int (if int can represent all values)
  • wchar_t, char16_t, char32_t, char8_t → the first of int, unsigned int, long unsigned long that 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 int or long for binary protocol fields. The size of int and long varies 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-overflow during testing to catch these at runtime. For production code, use std::cmp_less, std::cmp_greaterAnd std::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 or std::nextafter for 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), where long is 32 bits but void* is 64 bits. Use intptr_t or uintptr_t for 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. Use std::memcpy to 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-math or -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.

Worked Examples

See Also