<cstdint>
Fixed-width integer type aliases and limit macros for portable, size-explicit integer programming in C++.
<cstdint>since C++11A standard library header providing platform-independent type aliases for integers with guaranteed widths, along with macros for their minimum, maximum, and literal values.
Overview
When int is 16 bits on one platform and 32 bits on another, code that assumes a particular width silently breaks. <cstdint> solves this by providing type aliases with exact widths baked into their names. The header was formally incorporated into C++ with C++11, sourced from C99's <stdint.h>.
Four families of integer aliases are provided, each covering widths 8, 16, 32, and 64 bits:
| Family | Purpose | Example |
|---|---|---|
intN_t / uintN_t | Exact width; present if the platform supports it | uint32_t |
int_leastN_t / uint_leastN_t | Smallest type with at least N bits; always present | uint_least16_t |
int_fastN_t / uint_fastN_t | Fastest type with at least N bits; always present | int_fast32_t |
intmax_t / uintmax_t | Widest integer type the implementation supports | uintmax_t |
Two optional aliases, intptr_t and uintptr_t, hold a pointer value without loss of information. All major hosted implementations provide them, but the standard does not require it.
All aliases live in namespace std. The header also exposes the C limit and literal macros (INT32_MAX, UINT64_C(), etc.) in the global namespace β these are preprocessor macros and are not in std::.
Syntax
#include <cstdint>
// Exact-width signed
std::int8_t std::int16_t std::int32_t std::int64_t
// Exact-width unsigned
std::uint8_t std::uint16_t std::uint32_t std::uint64_t
// Minimum-width β always available
std::int_least8_t std::int_least16_t std::int_least32_t std::int_least64_t
std::uint_least8_t std::uint_least16_t std::uint_least32_t std::uint_least64_t
// Fastest minimum-width β always available
std::int_fast8_t std::int_fast16_t std::int_fast32_t std::int_fast64_t
std::uint_fast8_t std::uint_fast16_t std::uint_fast32_t std::uint_fast64_t
// Widest integer
std::intmax_t std::uintmax_t
// Pointer-sized β optional, present on all major platforms
std::intptr_t std::uintptr_t
// Limit macros (global namespace)
INT8_MIN INT8_MAX UINT8_MAX
INT16_MIN INT16_MAX UINT16_MAX
INT32_MIN INT32_MAX UINT32_MAX
INT64_MIN INT64_MAX UINT64_MAX
INTMAX_MIN INTMAX_MAX UINTMAX_MAX
// Integer constant macros β produce a literal of exactly the right type
INT8_C(v) INT16_C(v) INT32_C(v) INT64_C(v)
UINT8_C(v) UINT16_C(v) UINT32_C(v) UINT64_C(v)
INTMAX_C(v) UINTMAX_C(v)Examples
Binary protocol and network packet parsing
When you lay out a struct to match an on-wire format, exact widths are non-negotiable:
#include <cstdint>
#include <bit> // C++20
#include <cstring>
struct UDPHeader {
std::uint16_t src_port;
std::uint16_t dst_port;
std::uint16_t length;
std::uint16_t checksum;
};
static_assert(sizeof(UDPHeader) == 8);
UDPHeader parse_udp(const std::byte* buf) {
UDPHeader h;
std::memcpy(&h, buf, sizeof(h));
// Convert big-endian network byte order to host order
h.src_port = std::byteswap(h.src_port); // C++23
h.dst_port = std::byteswap(h.dst_port); // C++23
h.length = std::byteswap(h.length); // C++23
h.checksum = std::byteswap(h.checksum); // C++23
return h;
}The static_assert makes the size assumption explicit and catches unexpected padding at compile time rather than at runtime in the field.
Hardware register access and IP address overlays
Union overlays between raw integers and structured fields are a common pattern in systems programming:
#include <cstdint>
#include <format> // C++20
union IPv4Address {
struct { std::uint8_t a, b, c, d; } octets;
std::uint32_t raw;
};
void print_ip(std::uint32_t addr_le) {
IPv4Address ip;
ip.raw = addr_le;
// std::format β C++20
std::println("{}.{}.{}.{}", ip.octets.a, ip.octets.b, ip.octets.c, ip.octets.d);
}Enum underlying types
Since C++11, scoped enums can specify an underlying type. Fixed-width types make the ABI contract explicit:
#include <cstdint>
enum class Status : std::uint32_t { // C++11 β size is exactly 4 bytes everywhere
OK = 0,
Error = 1,
Timeout = 2,
};
enum class Flags : std::uint8_t { // C++11 β fits in one byte, no padding waste
None = 0x00,
Read = 0x01,
Write = 0x02,
Execute = 0x04,
};Forward-declaring a scoped enum with an underlying type lets the compiler know its size without seeing the enumerator list, enabling use across translation unit boundaries without dragging in the full definition.
Integer constant macros in constexpr and templates
INT32_C and friends produce a literal of exactly the specified type. Without them, an unadorned numeric literal is int, which can silently truncate or trigger UB in constant expressions:
#include <cstdint>
// Without UINT64_C, 0x1FFFF is int β value is fine here, but the intent is lost
constexpr std::uint64_t MAX_PRIME = UINT64_C(0x1FFFF);
template <std::uint32_t N>
struct Factorial {
static constexpr std::uint32_t value = N * Factorial<N - 1>::value;
};
template <>
struct Factorial<UINT32_C(0)> {
static constexpr std::uint32_t value = UINT32_C(1);
};
static_assert(Factorial<10>::value == 3628800);Lock-free atomics over fixed-width types
std::atomic specialisations over fixed-width types are lock-free on any platform where the hardware supports the native width natively:
#include <cstdint>
#include <atomic>
// C++11: likely lock-free on x86/ARM for 32- and 64-bit widths
std::atomic<std::uint32_t> counter{0};
// C++17: check lock-freedom at compile time
static_assert(std::atomic<std::uint32_t>::is_always_lock_free);
void increment() {
counter.fetch_add(1, std::memory_order_relaxed);
}is_always_lock_free (C++17) is true for uint32_t and uint64_t on every mainstream 64-bit architecture.
Best Practices
Use exact types when the width is a contract. Serialisation formats, hardware registers, network protocols, and persistent file layouts all have fixed binary layouts. Using uint32_t makes that contract explicit; using int leaves it to ABI luck and compiler flags.
Prefer the std:: qualified form. std::uint32_t is unambiguous and avoids the edge cases that arise when the C macros and typedefs bleed into the global namespace via transitive includes. The qualification is also a visual cue that the type is a guaranteed-width alias, not a built-in.
For pure performance, consider int_fast32_t. When you need at least 32 bits but do not care about the exact size, int_fast32_t lets the implementation choose the most efficient width. On most 64-bit Linux and macOS systems, int_fast32_t is 64 bits, giving better alignment properties on those targets.
Lock layout assumptions into static_assert. Whenever a struct matches an external binary format, assert sizeof and offsetof at compile time:
#include <cstdint>
#include <cstddef>
struct FileHeader {
std::uint32_t magic;
std::uint16_t version;
std::uint16_t flags;
};
static_assert(sizeof(FileHeader) == 8);
static_assert(offsetof(FileHeader, version) == 4);Common Pitfalls
Exact-width types are technically optional. The standard requires int8_t only if the platform has a two's-complement type of exactly 8 bits with no padding bits. Every hosted implementation on x86, ARM, RISC-V, and other mainstream architectures provides all eight exact-width types. Freestanding or exotic embedded targets may not; int_least8_t is the safe fallback since it is always defined.
int_fast8_t is not necessarily 8 bits. On many 64-bit Linux glibc implementations, int_fast8_t is long (64 bits). Code that assumes sizeof(int_fast8_t) == 1 is non-portable and will misbehave when iterated over arrays or used in packed structures. Use uint8_t when you need exactly one byte.
Format specifiers for printf/scanf live in <cinttypes>, not here. Printing a uint64_t with printf requires PRIu64 from <cinttypes>:
#include <cstdint>
#include <cinttypes>
#include <cstdio>
std::uint64_t n = UINT64_C(12345678901234);
std::printf("%" PRIu64 "\n", n); // correct on all platforms
// std::printf("%lu\n", n); // UB on Windows (LLP64): long is 32 bits thereWith C++20's std::format or C++23's std::println, the type system eliminates this class of error entirely β prefer them in new code.
Do not using-alias short names in headers. Aliases like using i32 = std::int32_t; in a header pollute every translation unit that includes it. Keep such conveniences in .cpp files or within a tightly scoped namespace.
Integer Promotion Pitfalls with Small Types
uint8_t and uint16_t are implicitly promoted to int in arithmetic. C++'s integer promotion rules convert any type narrower than int to int before performing arithmetic β the result type is int, not the narrow type. This is a frequent source of surprise.
#include <cstdint>
uint8_t a = 200, b = 100;
uint8_t c = a + b; // addition is done as int(200) + int(100) = int(300)
// then truncated to uint8_t: 300 % 256 == 44Comparison results can be counterintuitive when the promoted value does not wrap. Because the arithmetic is performed as int, overflow semantics differ from what you might expect from the narrow type:
#include <cstdint>
uint8_t x = 250;
if (x + 10 > 255) { // x + 10 is int(260), so 260 > 255 is true
// This branch IS taken β even though uint8_t(260) would wrap to 4
}Fix with an explicit cast when you need narrow-type truncation before comparing, or use uint32_t for intermediate arithmetic and cast back only on storage:
#include <cstdint>
uint8_t a = 200, b = 100;
uint8_t c = static_cast<uint8_t>(a + b); // explicit truncation β intent is clear
// Signed/unsigned mixed promotion: both sides go to int, so the signed value is preserved
int8_t sa = -1;
uint8_t ub = 1;
bool ok = (sa < ub); // int(-1) < int(1) β true; no UB here because both fit in intstd::bit_cast for Type-Punning (C++20)
Union overlay for type-punning is undefined behaviour under the strict aliasing rule. Before C++20, the only standards-conforming way to reinterpret object bytes was std::memcpy into a properly aligned destination. C++20 introduces std::bit_cast as a cleaner, constexpr-capable alternative.
#include <cstdint>
#include <bit> // C++20
// Inspect the IEEE 754 bit pattern of a float
constexpr uint32_t bits = std::bit_cast<uint32_t>(3.14f); // C++20 β constexpr capable
static_assert(bits == 0x4048F5C3u);std::bit_cast<To>(from) is well-defined when sizeof(To) == sizeof(From) and both types are trivially copyable. Unlike reinterpret_cast, it produces a new value rather than aliasing the original, so it never violates strict aliasing.
#include <cstdint>
#include <bit>
#include <array> // C++20
// Read a 4-byte big-endian integer from a byte buffer without UB
std::array<uint8_t, 4> buf = {0x01, 0x02, 0x03, 0x04};
uint32_t raw = std::bit_cast<uint32_t>(buf); // C++20
// raw == 0x01020304 on a big-endian host, 0x04030201 on little-endianmemcpy is still valid but not constexpr. Both approaches produce identical machine code after optimisation; prefer bit_cast in new C++20 code because it communicates intent and works in constant expressions.
Endianness and Byte Ordering
std::endian (C++20) lets you detect the host byte order at compile time. std::endian::native equals std::endian::little on x86 and modern ARM; it equals std::endian::big on PowerPC, SPARC, and most network-facing embedded MIPS targets.
#include <bit> // C++20
#include <cstdint>
constexpr bool is_little_endian = (std::endian::native == std::endian::little); // C++20std::byteswap<T>(v) (C++23) reverses the byte order of any unsigned integer type. Combined with an endianness check, it provides a portable way to read big-endian binary fields without relying on POSIX htonl/ntohl, which are not available on Windows without extra headers.
#include <bit> // C++23
#include <cstdint>
#include <cstring>
// Read a big-endian uint32_t field from a binary packet portably
uint32_t read_be32(const uint8_t* p) {
uint32_t v;
std::memcpy(&v, p, 4); // safe byte-exact copy
if constexpr (std::endian::native == std::endian::little) {
v = std::byteswap(v); // C++23
}
return v;
}Pre-C++23 alternatives: __builtin_bswap32 on GCC/Clang, _byteswap_ulong on MSVC. These are intrinsics, not standard, but they compile to a single bswap instruction on x86 and are universally available in practice.
__int128 and __uint128_t Extensions
GCC and Clang provide __int128 and __uint128_t on 64-bit platforms as a non-standard extension. These are not in <cstdint> and are not covered by std::numeric_limits, but they are indispensable for certain cryptographic and modular-arithmetic algorithms where 64-bit intermediate products overflow.
#include <cstdint>
// 64Γ64 β 128-bit multiply without overflow
uint64_t a = UINT64_MAX, b = UINT64_MAX;
__uint128_t wide = static_cast<__uint128_t>(a) * b; // GCC/Clang, 64-bit only
uint64_t hi = static_cast<uint64_t>(wide >> 64);
uint64_t lo = static_cast<uint64_t>(wide);__int128 has no printf specifier and no std::format support out of the box. To print one, shift-and-cast to uint64_t chunks, or write a custom std::formatter specialisation:
#include <cstdint>
#include <format> // C++20
// Custom formatter for __uint128_t β production code should handle all bases
template <>
struct std::formatter<__uint128_t> {
constexpr auto parse(std::format_parse_context& ctx) { return ctx.begin(); }
auto format(__uint128_t v, std::format_context& ctx) const {
if (v == 0) return std::format_to(ctx.out(), "0");
char buf[40]; int n = 0;
for (; v; v /= 10) buf[n++] = '0' + static_cast<int>(v % 10);
for (int i = n - 1; i >= 0; --i)
*ctx.out()++ = buf[i];
return ctx.out();
}
};MSVC does not provide a scalar __int128. The closest equivalent is __m128i, which is a SIMD register type, not a general-purpose integer. Guard platform-specific code accordingly:
#if defined(__GNUC__) || defined(__clang__)
using u128 = __uint128_t;
#else
// MSVC: fall back to two uint64_t fields + manual carry arithmetic
#endifC++26 proposals P0907 and P1730 aim to standardise std::int128_t / std::uint128_t, but as of 2026 neither is merged into the working draft.
intptr_t and Pointer-as-Integer Patterns
intptr_t and uintptr_t are the only integer types guaranteed to round-trip a pointer. The standard guarantees that reinterpret_cast<uintptr_t>(p) followed by reinterpret_cast<void*>(v) yields the original pointer; arithmetic on the integer value in between is not guaranteed to produce a valid pointer, though it works on all flat-address-space architectures in practice.
#include <cstdint>
#include <cassert>
void* p = /* some 64-byte aligned allocation */;
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
// Alignment check β well-defined, no pointer arithmetic
assert((addr & 63u) == 0); // verify 64-byte alignment
// Tagged pointer: store a 3-bit tag in the low bits of an 8-byte aligned pointer
struct TaggedPtr {
uintptr_t bits;
void* ptr() const { return reinterpret_cast<void*>(bits & ~uintptr_t{7}); }
uint8_t tag() const { return static_cast<uint8_t>(bits & 7u); }
static TaggedPtr make(void* p, uint8_t tag) {
return {reinterpret_cast<uintptr_t>(p) | (tag & 7u)};
}
};ptrdiff_t and intptr_t are related but distinct. ptrdiff_t (from <cstddef>) is the signed result of subtracting two pointers into the same array; it may be narrower than intptr_t on platforms where the pointer space exceeds the difference range. Use ptrdiff_t for pointer differences and intptr_t/uintptr_t only when you genuinely need to treat a pointer as an integer.
std::numeric_limits vs C Macros
INT32_MAX, UINT64_MAX, and friends are preprocessor macros β they work in #if directives but are hostile to templates. std::numeric_limits<T>::max() is constexpr, type-safe, and composable with generic code.
#include <cstdint>
#include <limits> // C++98
// Generic overflow-safe addition β impossible to write with C macros
template <typename T>
bool would_overflow_add(T a, T b) {
return a > std::numeric_limits<T>::max() - b; // C++11 constexpr
}
static_assert(would_overflow_add<uint32_t>(UINT32_MAX, 1u));
static_assert(!would_overflow_add<uint32_t>(1u, 1u));std::numeric_limits<T>::digits gives the number of value bits, excluding the sign bit. Adding is_signed recovers the total bit width without hardcoding a number:
#include <cstdint>
#include <limits>
constexpr int bits_in_uint32 =
std::numeric_limits<uint32_t>::digits +
std::numeric_limits<uint32_t>::is_signed; // 32 + 0 = 32
constexpr int bits_in_int64 =
std::numeric_limits<int64_t>::digits +
std::numeric_limits<int64_t>::is_signed; // 63 + 1 = 64C++20 adds std::bit_width(x) for the minimum number of bits needed to represent a non-negative integer value. This is distinct from the storage width of the type and is useful when packing variable-length fields:
#include <cstdint>
#include <bit> // C++20
uint32_t val = 255;
unsigned w = std::bit_width(val); // C++20 β returns 8 (since 2^8 > 255 >= 2^7)Prefer std::numeric_limits over the C macros in all new C++11 or later code. Keep the macros only where preprocessor conditionals or C interoperability require them.
See Also
<cinttypes>β format macros (PRId32,SCNu64) forprintf/scanfwith fixed-width types<limits>βstd::numeric_limits<T>provides the same bounds as the C macros in a type-safe, template-friendly form (C++98)<atomic>β lock-free operations over fixed-width integer specialisations (C++11)<bit>β bit-manipulation utilities includingstd::byteswap(C++23) andstd::popcount(C++20), which pair naturally with fixed-width types