Fundamental Types
Complete reference for C++ fundamental types — void, bool, character, integer, and floating-point types — including sizes, ranges, and standard guarantees.
Fundamental TypesThe set of types built directly into the C++ language — void, bool, character types, integer types, and floating-point types — that directly map to hardware-native storage and operations, from which all compound and user-defined types are built.
Overview
Fundamental types form the foundation of the C++ type system. They are specified by the language standard itself, not defined in any library, and correspond directly to hardware-level storage units. Every other type in C++ — arrays, pointers, references, class types — is either composed of or built atop fundamental types.
The standard divides fundamental types into three groups:
void— the incomplete type representing an empty value set- Integral types —
bool, character types, and integer types - Floating-point types —
float,double,long double
One critical property of fundamental types: their sizes, ranges, and alignment are implementation-defined in the core standard. The standard only mandates minimum widths and ordering relationships. This is distinct from fixed-width integer types (introduced in <cstdint> in C++11), which provide exact-width guarantees.
Void
void is an incomplete type with an empty value set. Objects of type void cannot be created, and there are no references to void. It appears as a function return type signaling no value is produced, as the universal pointer base type (void*), and in template metaprogramming.
void process(int x); // returns nothing
void* raw = malloc(64); // untyped pointer
template<typename T> void log(T val);Boolean
bool stores true or false. Arithmetic on bool is permitted — true promotes to 1 and false to 0 — but this is rarely intentional and often a bug. The size of bool is implementation-defined but is always at least 1 byte.
bool flag = true;
int count = flag + 2; // 3 — legal but usually unintentionalCharacter Types
C++ has six character types. They differ in signedness, width, and encoding semantics:
| Type | Since | Notes |
|---|---|---|
char | C++98 | 1 byte; signedness is implementation-defined |
signed char | C++98 | 1 byte, signed |
unsigned char | C++98 | 1 byte, unsigned; the canonical byte type |
wchar_t | C++98 | Wide char; 2 bytes on Windows, 4 on Linux |
char16_t | C++11 | UTF-16 code unit; at least 16 bits |
char32_t | C++11 | UTF-32 code unit; at least 32 bits |
char8_t | C++20 | UTF-8 code unit; same representation as unsigned char |
char being possibly signed is a persistent source of bugs. Comparing a char to EOF (-1) works on platforms where char is signed but silently fails on platforms where it is unsigned.
// Portable character inspection — always use unsigned char for byte math
unsigned char byte = static_cast<unsigned char>(ch);
bool is_high_bit_set = (byte & 0x80) != 0;
// char8_t for UTF-8 (C++20)
const char8_t* utf8 = u8"héllo"; // C++20Integer Types
The signed integer types, from narrowest to widest: short int, int, long int, and long long int (C++11). The standard guarantees only a minimum width and the ordering relationship:
sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)Minimum guaranteed widths: short ≥ 16 bits, int ≥ 16 bits (in practice 32 on all modern targets), long ≥ 32 bits, long long ≥ 64 bits.
Each signed type has a corresponding unsigned variant. The int keyword may be omitted when other modifiers are present: unsigned alone is unsigned int, long alone is long int.
short s = 32767;
unsigned short us = 65535;
int i = -2'147'483'648; // digit separators since C++14
long l = 2'147'483'647L;
long long ll = 9'223'372'036'854'775'807LL; // C++11
unsigned long long ull = 18'446'744'073'709'551'615ULL;Fixed-Width Integer Types (C++11)
When exact widths matter — serialization, protocol headers, bit manipulation — use the types from <cstdint>:
#include <cstdint> // C++11
int8_t a = -128;
uint8_t b = 255;
int16_t c = -32768;
uint32_t d = 0xDEAD'BEEF;
int64_t e = -1;
// Fastest type of at least N bits — avoid assuming a specific width
int_fast32_t fast = 42;
// Smallest type of at least N bits — for space-constrained storage
int_least8_t small = 0;int8_t, uint8_t, etc. are optional (the standard permits omitting them if no type of that exact width exists on the platform), but they are available on all practical targets.
Floating-Point Types
Three floating-point types exist: float, double, and long double. On nearly all modern platforms these follow IEEE 754:
float— single precision (32-bit); ~7 significant decimal digitsdouble— double precision (64-bit); ~15 significant decimal digitslong double— extended or quad precision; 80-bit on x86 with extended precision, 128-bit on some RISC platforms, 64-bit (same asdouble) on MSVC
float f = 3.14f; // f suffix required for float literals
double d = 3.14159265358979;
long double ld = 3.14159265358979323846L; // L suffix
// Avoid mixing float and double implicitly — promotion rules apply
float result = f + 1.0; // 1.0 is double; f is promoted, result truncated back to floatSince C++23, <stdfloat> introduces optional extended floating-point types (std::float16_t, std::float32_t, std::float64_t, std::float128_t, std::bfloat16_t) for hardware acceleration and ML workloads — but availability remains platform-dependent.
Querying Type Properties
Never hardcode sizes or ranges. Use standard facilities:
#include <climits> // INT_MAX, CHAR_BIT, etc.
#include <cfloat> // FLT_MAX, DBL_EPSILON, etc.
#include <limits> // std::numeric_limits (C++98)
static_assert(sizeof(int) >= 4, "need 32-bit int");
auto max_int = std::numeric_limits<int>::max(); // 2147483647 typically
auto eps = std::numeric_limits<double>::epsilon(); // ~2.2e-16
auto inf = std::numeric_limits<float>::infinity(); // C++98
bool has_inf = std::numeric_limits<double>::has_infinity; // true for IEEE 754Initialization
Fundamental types have two distinct default-initialization behaviors: uninitialized when declared locally (indeterminate value, undefined behavior to read), and zero-initialized in static or thread-local storage.
int x; // local — indeterminate; reading x is UB
static int y; // static storage — zero-initialized to 0
int a{}; // value-initialization: a == 0 (C++11 brace syntax)
int b = int(); // value-initialization: b == 0 (pre-C++11)
int c(0); // direct-initializationPrefer {} value-initialization for fundamental types when you need a guaranteed zero, and brace initialization catches narrowing conversions at compile time.
Best Practices
- Use
intfor general-purpose integer arithmetic unless you have a measured reason not to. Premature use ofshortorunsignedintroduces subtle bugs. - Use
unsignedtypes only when you need modular arithmetic or bitwise operations, not merely to express "non-negative" — mixing signed and unsigned in expressions triggers implicit conversion and comparison surprises. - Prefer
doubleoverfloatfor floating-point work unless you have a performance or memory constraint; the precision difference has caused hard-to-diagnose numerical errors. - Use fixed-width types (
uint32_t, etc.) at ABI boundaries, in serialization code, and wherever exact width matters. - Never rely on
sizeof(int) == 4being universally true; usestatic_assertto guard platform assumptions.
Common Pitfalls
Signed integer overflow is undefined behavior. Compilers exploit this for optimization, which can eliminate bounds checks entirely:
int x = INT_MAX;
bool wrapped = (x + 1 < x); // UB — compiler may optimize to falsechar signedness mismatch: Comparing plain char with negative values (like EOF) is undefined if char is unsigned on the target. Use unsigned char or int for character-by-character I/O.
Floating-point equality: Comparing floating-point values with == is almost never correct for computed results. Use an epsilon comparison or std::abs(a - b) < tolerance.
double a = 0.1 + 0.2;
double b = 0.3;
bool eq1 = (a == b); // false — do not do this
bool eq2 = std::abs(a - b) < 1e-9; // correct approachImplicit narrowing: Assigning a double to a float or an int to a short silently truncates in C-style initialization. Brace initialization rejects narrowing at compile time:
double d = 3.14;
float f1 = d; // silently narrows
float f2{d}; // error: narrowing conversion (C++11)Platform Width Guarantees (Data Models)
The standard only mandates minimums and the ordering relationship sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long). Each platform chooses a data model that assigns concrete widths. The four models in common use:
| Type | C++ minimum | LP32 (Win16) | ILP32 (Win32, Linux/macOS 32-bit) | LLP64 (Windows 64-bit) | LP64 (Linux/macOS 64-bit) |
|---|---|---|---|---|---|
char | 8 bits | 8 | 8 | 8 | 8 |
short | 16 bits | 16 | 16 | 16 | 16 |
int | 16 bits | 16 | 32 | 32 | 32 |
long | 32 bits | 32 | 32 | 32 | 64 |
long long | 64 bits | 64 | 64 | 64 | 64 |
| pointer | — | 32 | 32 | 64 | 64 |
The critical divergence is long: it is 32 bits on Windows (LLP64) but 64 bits on Linux and macOS (LP64). Code that assumes long is 64 bits will silently truncate on Windows; code that assumes it is 32 bits loses data on LP64 systems. long long is the only portable way to guarantee a 64-bit signed integer in standard C++ prior to <cstdint>.
// Portable: long long is always >= 64 bits
long long file_size = get_file_size();
// NOT portable: long is 32 bits on Windows 64-bit
long file_size_bad = get_file_size(); // truncates on LLP64 if size > 2^31-1
// Best for exact widths: use <cstdint>
#include <cstdint>
int64_t file_size_best = get_file_size();char Signedness
char, signed char, and unsigned char are three distinct types even though they share the same size and bit representation. The signedness of plain char is implementation-defined:
- x86 / x86-64 (Linux, macOS, Windows):
charis signed by default — range[-128, 127] - ARM / PowerPC (many embedded targets, older Android NDK):
charis unsigned by default — range[0, 255]
You can override the default with compiler flags: -fsigned-char / -funsigned-char (GCC/Clang) or /J (MSVC).
char c = 200; // value 200 on unsigned-char platforms; -56 on signed-char platforms
// Classic EOF bug — EOF is -1 (int); if char is unsigned, this loop never terminates
while ((char ch = getchar()) != EOF) { /* ... */ } // WRONG if char is unsigned
// Correct: read into int, compare, then use
int ch;
while ((ch = getchar()) != EOF) {
process(static_cast<unsigned char>(ch)); // safe byte math
}The rule of thumb: use char for text, unsigned char for raw bytes and bit manipulation, signed char when you explicitly need a signed 8-bit integer. Never mix plain char with bit operations that depend on the sign bit.
Fixed-Width Integer Types and Fundamental Types
<cstdint> (C++11) provides typedefs over fundamental types, not new types. On a typical LP64 Linux system:
int8_t = signed char
int16_t = short
int32_t = int
int64_t = long // LP64 only — long long on LLP64 (Windows)
uint32_t = unsigned int
uint64_t = unsigned long // LP64 onlyOn a different platform the mapping changes, but the typedef name stays constant. This is the core value: you name the contract, not the implementation.
The types come in three families:
| Family | Example | Guarantee |
|---|---|---|
| Exact-width | int32_t | Exactly N bits; optional (absent if no type exists) |
| Minimum-width | int_least32_t | At least N bits; always present |
| Fastest minimum-width | int_fast32_t | Fastest type of at least N bits; always present |
int8_t and uint8_t may be aliases of signed char and unsigned char respectively — which means they may print as characters rather than integers with std::cout. Cast to int before printing if that matters.
#include <cstdint>
#include <iostream>
uint8_t b = 65;
std::cout << b; // prints 'A' — it's unsigned char!
std::cout << static_cast<int>(b); // prints 65
// Prefer fixed-width types at ABI and serialization boundaries
struct PacketHeader { uint16_t length; uint32_t sequence; uint8_t flags; };When to use each:
- Fundamental types (
int,long long): general computation, loop counters, return values — the compiler can choose the best fit - Fixed-width (
uint32_t,int64_t): serialization, file formats, network protocols, hardware registers, anywhere exact width is load-bearing - Minimum-width (
int_least32_t): portable code that needs at least N bits but doesn't need exactly N
bool: Size vs. Logical Width
bool is logically a single bit but physically occupies at least one byte (on all common ABIs, sizeof(bool) == 1). This is because C++ objects must be individually addressable, and the smallest addressable unit is a byte.
bool flag = true;
std::cout << sizeof(bool); // 1 on x86, x86-64, ARM — implementation-defined but universally 1
struct Flags {
bool a, b, c; // 3 bytes, not 3 bits
};
std::cout << sizeof(Flags); // 3 (plus possible padding depending on struct alignment)std::vector<bool> is the notable exception: it is a standard-mandated space optimization that packs 8 booleans into one byte. This makes it non-conforming to the general vector requirements — you cannot take the address of an element, and operator[] returns a proxy object, not a bool&.
#include <vector>
std::vector<bool> bits(8, false);
// bits[3] = true; // works through proxy
// bool* p = &bits[3]; // ERROR — no address of individual bit
// Use std::vector<char> or std::vector<uint8_t> if you need addressable elements
std::vector<uint8_t> bools(8, 0);
uint8_t* p = &bools[3]; // fine — each element is a full byteFor a fixed-size bit array, std::bitset<N> provides bit-level storage without the proxy-object surprises.
_BitInt(N) — Bit-Precise Integers (C++26)
C23 introduced _BitInt(N), a family of integer types where N is an exact bit width specified at compile time. C++26 is standardizing this feature (proposal P3666) for compatibility with C and to support computation beyond 64 bits natively.
_BitInt(N) is distinct from <cstdint> fixed-width types: it allows non-power-of-two widths (e.g., _BitInt(3), _BitInt(127), _BitInt(4096)) and operates without integer promotion to int.
// C++26 / C23: bit-precise integers
_BitInt(3) x = 3; // 3-bit signed: range [-4, 3]
unsigned _BitInt(8) u = 200U; // 8-bit unsigned (same as uint8_t, but distinct type)
_BitInt(128) big = 0; // 128-bit signed integer — no library needed
_BitInt(4096) crypto_key; // 4096-bit integer for RSA-scale arithmetic
// Literal suffix 'wb' (unsigned) or 'uwb'
auto a = 42wb; // _BitInt deduced from value
auto b = 0xFFFFuwb; // unsigned _BitInt(16) literalKey behavioral rules:
- No integer promotion:
_BitInt(8) + _BitInt(8)stays_BitInt(8), notint. This prevents the truncation surprise ofuint8_tarithmetic. - Mixed arithmetic:
_BitInt(8) + intproducesint(bit-precise types have lower conversion rank than standard integers unless their width exceedsint). - Width limit:
BITINT_MAXWIDTH— mandated to be at least 64; in practice implementations may support up to 65,535 bits or more.
Practical use cases:
// Cryptography: RSA key material
_BitInt(2048) rsa_modulus;
// Bit-packing: exact-width fields for FPGA or hardware register maps
struct RGB10 { unsigned _BitInt(10) r, g, b; }; // 10-bit channels, no masking needed
// 128-bit arithmetic for hash mixing without __int128 extension
_BitInt(128) hash_state = initial_value;
hash_state = hash_state * 6364136223846793005wb + 1442695040888963407wb;Compiler support (as of 2026): Clang has supported _BitInt since version 14 (via the older _ExtInt extension). GCC added support in GCC 14. MSVC support is in progress. Check __has_extension(c_bit_precise_int) (Clang) or feature-test macros before relying on it.
#if defined(__cpp_bit_precise_integers) // C++ feature-test macro (proposed)
_BitInt(128) wide = compute_128bit();
#else
// fallback: __int128 (GCC/Clang extension, not standard)
__int128 wide = compute_128bit();
#endifFixed-Width Integer Types in Depth
<cstdint> types make ABI contracts explicit. When you write uint32_t in a struct that gets serialized to disk or sent over a network, every reader on every platform sees exactly the same 32 bits. When you write int, a reader on a platform with a different data model may see 16 or 64 bits instead.
// C++11
#include <cstdint>
#include <cstddef> // ptrdiff_t, size_t
// Exact-width types — use at serialization/ABI boundaries
int32_t sensor_reading = -1024;
uint64_t packet_counter = 0;
// intptr_t / uintptr_t — integer wide enough to hold a pointer value
// Use for pointer arithmetic, NOT for storing arbitrary integers
uintptr_t addr = reinterpret_cast<uintptr_t>(some_ptr);
// ptrdiff_t — signed result of subtracting two pointers
int arr[8];
ptrdiff_t dist = &arr[5] - &arr[0]; // 5; always signed
// size_t — result of sizeof, array indices, container sizes
size_t n = sizeof(double); // 8 on most platformsintptr_t and uintptr_t are optional in the standard but present on all hosted platforms; they are the only safe integer types for round-tripping a pointer through an integer. ptrdiff_t is always signed — using size_t for pointer differences silently wraps on subtraction that yields a negative value.
// C++11
#include <cstdint>
// Prefer fixed-width types over raw fundamental types at these sites:
struct WireHeader {
uint16_t version; // NOT unsigned short — which is 16 bits, but spell it out
uint32_t payload_len; // NOT unsigned int — 32 vs 64 ambiguity on LP64
uint64_t timestamp_ns; // NOT unsigned long — 32 bits on Windows LLP64
};
// Minimum-width family — guaranteed present even on exotic platforms
int_least16_t small_val = 0; // at least 16 bits; may be wider
// Fastest family — compiler picks the widest register-friendly type
int_fast32_t loop_var = 0; // may be 64 bits on LP64 — don't assume sizeofWhen to reach for each family: exact-width (int32_t) at ABI boundaries; minimum-width (int_least32_t) for portable storage where a bit more width is harmless; fastest (int_fast32_t) in tight loops when you only care about performance and the value fits in 32 bits logically.
Extended Floating-Point Types (C++23)
C++23 adds optional hardware-native float types via <stdfloat>. Machine-learning accelerators and GPU compute kernels routinely operate on 16-bit floats; the standard finally gives them a first-class home.
// C++23
#include <stdfloat>
std::float16_t f16 = 1.5f16; // IEEE 754 binary16: 1 sign + 5 exp + 10 mantissa
std::bfloat16_t bf = 1.5bf16; // brain float: 1 sign + 8 exp + 7 mantissa (same exp range as float32)
std::float32_t f32 = 3.14f32; // same as float on IEEE 754 platforms, but spelled explicitly
std::float64_t f64 = 3.14f64; // same as double on IEEE 754 platforms
std::float128_t f128 = 3.14f128; // quad precision where availablestd::bfloat16_t (brain float) is favored in ML workloads because it preserves the exponent range of 32-bit float, making it easy to quantize models without re-scaling gradients. std::float16_t has a narrower exponent range but 3 extra mantissa bits, making it more accurate for inference near-zero values. All five types are optional — check for __STDCPP_FLOAT16_T__ and friends before using them.
// C++23
#ifdef __STDCPP_FLOAT16_T__
#include <stdfloat>
#include <vector>
// Pack a weight matrix in half-precision for GPU upload
std::vector<std::float16_t> weights(1024 * 1024);
for (auto& w : weights)
w = static_cast<std::float16_t>(compute_weight());
#endifArithmetic on extended float types follows the same promotion rules as float and double, but mixing std::float16_t with double in the same expression produces a double. Do not mix extended types with fundamental types in performance-sensitive code without profiling — implicit widening may defeat the purpose.
std::byte — Type-Safe Bytes
std::byte is the correct type for raw memory, not unsigned char or char. Introduced in C++17 via <cstddef>, it has no integer semantics: you cannot add two std::byte values or compare them with <. Only bitwise operations and explicit conversions are allowed, preventing accidental arithmetic on raw memory.
// C++17
#include <cstddef>
std::byte b1{0b1010'0101};
std::byte b2{0xFF};
// Bitwise operations are permitted
std::byte result = b1 & b2; // bitwise AND
result = b1 | b2; // bitwise OR
result = b1 ^ b2; // bitwise XOR
result = ~b1; // complement
result = b1 << 2; // left shift
result = b1 >> 1; // right shift
// Arithmetic is NOT permitted — compiler error
// std::byte sum = b1 + b2; // error: no operator+
// if (b1 < b2) { } // error: no operator<Converting to and from integers requires std::to_integer<T> or static_cast — the explicitness is the point.
// C++17
#include <cstddef>
std::byte raw{42};
// Extract the integer value
int val = std::to_integer<int>(raw); // 42
unsigned u = std::to_integer<unsigned>(raw);
// Construct from integer — must be explicit
std::byte from_int = static_cast<std::byte>(255);
// std::byte bad = 255; // error: no implicit conversion from int
// Typical use: operating on a buffer of raw memory
void xor_buffer(std::byte* buf, std::size_t len, std::byte key) {
for (std::size_t i = 0; i < len; ++i)
buf[i] ^= key;
}Use std::byte whenever you are manipulating raw memory (serialization buffers, crypto, memory-mapped I/O) and want the type system to enforce that you are not accidentally doing integer arithmetic on bytes. Continue using unsigned char when you need to interact with C APIs that expect unsigned char* — the two types interconvert freely through static_cast.
Numeric Limits
std::numeric_limits<T> is the portable, type-generic way to query every property of any arithmetic type. The C macros (INT_MAX, DBL_EPSILON) only cover specific built-in types and do not work in templates; std::numeric_limits works uniformly across fundamental types, <cstdint> typedefs, and user-defined arithmetic types.
// C++11
#include <limits>
#include <cstdint>
// Signed integer extremes
auto max_i32 = std::numeric_limits<int32_t>::max(); // 2147483647
auto min_i32 = std::numeric_limits<int32_t>::min(); // -2147483648
auto max_u64 = std::numeric_limits<uint64_t>::max(); // 18446744073709551615
// Floating-point properties
auto eps_f = std::numeric_limits<float>::epsilon(); // ~1.19e-7
auto eps_d = std::numeric_limits<double>::epsilon(); // ~2.22e-16
auto inf_d = std::numeric_limits<double>::infinity(); // +inf (if available)
auto nan_d = std::numeric_limits<double>::quiet_NaN(); // a quiet NaN value
bool iec559 = std::numeric_limits<float>::is_iec559; // true on IEEE 754 platforms
// Useful in templates
template<typename T>
T safe_add(T a, T b) {
if (b > 0 && a > std::numeric_limits<T>::max() - b)
throw std::overflow_error{"overflow"};
return a + b;
}std::numeric_limits<float>::is_iec559 (true on virtually all modern targets) signals that the platform follows IEEE 754, which guarantees the behavior of infinity, NaN, subnormals, and rounding modes — code that branches on this flag can safely use std::isinf, std::isnan, and friends.
The C-era macros (INT_MAX, FLT_EPSILON) remain valid but have two drawbacks: they are not available for <cstdint> typedefs (there is no INT32_MAX in the core standard, though <cstdint> does provide it), and they cannot be used inside templates parameterized on a type. Prefer std::numeric_limits in new code.
// C++11
#include <limits>
#include <cstdint>
#include <climits> // INT_MAX macro — legacy, but occasionally clearer in non-generic code
// Equivalent — prefer the <limits> form in templates
static_assert(std::numeric_limits<int>::max() == INT_MAX,
"numeric_limits and INT_MAX must agree");
// digits: number of non-sign bits (useful for bit shifting)
constexpr int int_bits = std::numeric_limits<int>::digits; // 31 for 32-bit int
constexpr int uint_bits = std::numeric_limits<unsigned>::digits; // 32
// lowest() vs min() — for floats they differ!
// min() returns the smallest *positive normalized* value
// lowest() returns the most negative finite value
auto float_min = std::numeric_limits<float>::min(); // ~1.18e-38 (tiny positive)
auto float_lowest = std::numeric_limits<float>::lowest(); // ~-3.4e+38 (most negative)Always use lowest() rather than min() when you want the most negative representable value for a floating-point type — min() for floats is a notorious gotcha that returns the smallest positive normalized number, not the most negative finite number.
See Also
auto— let the compiler deduce the type of a variable from its initializerstd::bit_cast— type-safe reinterpretation of the object representation of fundamental types (C++20)- Aggregate Initialization — how fundamental types inside aggregates are zero-initialized