Contents
  1. What the Standard Actually Guarantees
  2. The Problem in Practice
  3. Fixed-Width Integers from <cstdint>
  4. When to Use Fixed-Width Types
  5. std::size_t
  6. What You Can Do Now
← All posts

C++ Primitive Types Are Not What You Think: Fixed-Width Integers and Why They Exist

C++ primitive types like int and long do not have guaranteed sizes. What your compiler gives you depends on the target architecture. Fixed-width integers from cstdint exist to fix exactly that.

Every C++ textbook introduces the same table: int is 4 bytes, long int is 8 bytes, char is 1 byte. That table is not wrong, but it is incomplete. Those numbers describe what is typical on a modern 64-bit desktop system. They are not what the C++ standard guarantees. On a different architecture, a different compiler, or an embedded target, the same types can have entirely different sizes. Code that assumes int is 32 bits can overflow silently on a system where it is 16.

What the Standard Actually Guarantees

The C++ standard specifies minimum sizes, not exact sizes. The guarantee is a floor, not a fixed value.

TypeStandard minimumTypical on 64-bit
char1 byte1 byte
short2 bytes2 bytes
int2 bytes4 bytes
long int4 bytes8 bytes on Linux/macOS, 4 bytes on Windows
long long int8 bytes8 bytes
float4 bytes4 bytes
double8 bytes8 bytes
bool1 byte1 byte

The most common surprise is long int. On a 64-bit Linux or macOS system it is 8 bytes. On a 64-bit Windows system it is 4 bytes. Code that relies on long being 64 bits will behave differently between these two platforms without any warning.

The reason for this flexibility is historical. C, and by extension C++, was designed to run on a wide range of hardware. Leaving integer sizes flexible allowed compiler authors to choose the size that performed best on each target. That was a reasonable trade-off in the 1970s. It creates portability problems today.

The Problem in Practice

Consider a function that encodes a timestamp as a single integer. If int is 32 bits, the maximum value is 2,147,483,647. That overflows in 2038, a known real-world problem. If the developer assumed long was 64 bits on all platforms but the code runs on Windows where long is 32 bits, the overflow happens there too.

#include <iostream>

int main() {
    // What does sizeof tell you on your machine?
    std::cout << "sizeof(int):      " << sizeof(int)      << " bytes\n";
    std::cout << "sizeof(long):     " << sizeof(long)     << " bytes\n";
    std::cout << "sizeof(long long):" << sizeof(long long) << " bytes\n";
    return 0;
}

Run this on Linux and Windows. The long line will differ. The standard permits both results.

Fixed-Width Integers from <cstdint>

C++11 introduced <cstdint>, a header that provides integer types with guaranteed sizes regardless of platform or compiler. These types are defined in terms of exact bit widths.

#include <cstdint>

std::int8_t   a;  // exactly 8 bits,  signed
std::uint8_t  b;  // exactly 8 bits,  unsigned
std::int16_t  c;  // exactly 16 bits, signed
std::uint16_t d;  // exactly 16 bits, unsigned
std::int32_t  e;  // exactly 32 bits, signed
std::uint32_t f;  // exactly 32 bits, unsigned
std::int64_t  g;  // exactly 64 bits, signed
std::uint64_t h;  // exactly 64 bits, unsigned

uint32_t is always 32 bits unsigned. On every architecture. On every compiler that supports the standard. There is no ambiguity.

When to Use Fixed-Width Types

Use fixed-width types whenever the size of the value matters for correctness:

  • Network protocols and file formats where bytes are laid out precisely
  • Hardware registers and memory-mapped I/O in embedded systems
  • Binary serialisation and deserialisation
  • Cryptographic operations that depend on exact bit widths
  • Interfacing with C libraries or external APIs that specify exact types

Use plain int when the value will always fit safely within any reasonable interpretation of the type, such as loop counters, array indices within known bounds, or values that will never exceed a few thousand.

#include <cstdint>
#include <iostream>

// Correct: network packet field is always exactly 32 bits
struct PacketHeader {
    uint32_t sequence_number;
    uint32_t timestamp;
    uint16_t payload_length;
    uint8_t  flags;
};

// Fine: loop counter, size fits in any int
for (int i = 0; i < 100; i++) {
    // ...
}

std::size_t

One additional type worth knowing is std::size_t, defined in <cstddef>. It is the type returned by sizeof and used by the standard library for sizes and indices. It is unsigned and wide enough to represent any object size on the current architecture, meaning 32 bits on a 32-bit system and 64 bits on a 64-bit system.

Use std::size_t when working with container sizes, array lengths, and memory offsets. Comparing a std::size_t with a signed int generates compiler warnings and can cause subtle bugs when the signed value is negative.

#include <cstddef>
#include <vector>

std::vector<int> v = {1, 2, 3, 4, 5};

for (std::size_t i = 0; i < v.size(); i++) {
    // v.size() returns std::size_t — matching types avoids signed/unsigned comparison warnings
    std::cout << v[i] << "\n";
}

What You Can Do Now

Open any C++ project you have and search for uses of int, long, or unsigned int in contexts where the exact size matters, such as structs that represent binary data, protocol fields, or values written to files or sent over a network. Replace them with the appropriate fixed-width type from <cstdint>.

Then verify your assumptions about your own platform:

#include <cstdint>
#include <cstddef>
#include <iostream>

int main() {
    std::cout << "int:       " << sizeof(int)       << " bytes\n";
    std::cout << "long:      " << sizeof(long)      << " bytes\n";
    std::cout << "long long: " << sizeof(long long) << " bytes\n";
    std::cout << "uint32_t:  " << sizeof(uint32_t)  << " bytes\n";
    std::cout << "uint64_t:  " << sizeof(uint64_t)  << " bytes\n";
    std::cout << "size_t:    " << sizeof(std::size_t) << " bytes\n";
    return 0;
}

The uint32_t and uint64_t lines will always read 4 and 8. The long line depends on where you run it. That difference is the entire argument for fixed-width types.

← All posts