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)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