Skip to content
C++
Language
Basic

Fundamental Types

Complete reference for C++ fundamental types — void, bool, character, integer, and floating-point types — including sizes, ranges, and standard guarantees.

Fundamental Types

The 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 typesbool, character types, and integer types
  • Floating-point typesfloat, 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.

cpp
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.

cpp
bool flag = true;
int count = flag + 2;  // 3 — legal but usually unintentional

Character Types

C++ has six character types. They differ in signedness, width, and encoding semantics:

TypeSinceNotes
charC++981 byte; signedness is implementation-defined
signed charC++981 byte, signed
unsigned charC++981 byte, unsigned; the canonical byte type
wchar_tC++98Wide char; 2 bytes on Windows, 4 on Linux
char16_tC++11UTF-16 code unit; at least 16 bits
char32_tC++11UTF-32 code unit; at least 32 bits
char8_tC++20UTF-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.

cpp
// 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++20

Integer 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:

cpp
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.

cpp
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>:

cpp
#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 digits
  • double — double precision (64-bit); ~15 significant decimal digits
  • long double — extended or quad precision; 80-bit on x86 with extended precision, 128-bit on some RISC platforms, 64-bit (same as double) on MSVC
cpp
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 float

Since 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:

cpp
#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 754

Initialization

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.

cpp
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-initialization

Prefer {} value-initialization for fundamental types when you need a guaranteed zero, and brace initialization catches narrowing conversions at compile time.

Best Practices

  • Use int for general-purpose integer arithmetic unless you have a measured reason not to. Premature use of short or unsigned introduces subtle bugs.
  • Use unsigned types 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 double over float for 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) == 4 being universally true; use static_assert to guard platform assumptions.

Common Pitfalls

Signed integer overflow is undefined behavior. Compilers exploit this for optimization, which can eliminate bounds checks entirely:

cpp
int x = INT_MAX;
bool wrapped = (x + 1 < x);  // UB — compiler may optimize to false

char 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.

cpp
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 approach

Implicit 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:

cpp
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 initializer
  • std::bit_cast — type-safe reinterpretation of the object representation of fundamental types (C++20)
  • Aggregate Initialization — how fundamental types inside aggregates are zero-initialized