Types, Values & the Type System
Explore C++'s fundamental types, fixed-width integers, initialization forms, auto deduction, and the const system that enforces correctness at compile time.
The Type System: Your First Line of Defense
C++ is a statically typed language — every expression has a type known at compile time. The type system is not just bookkeeping; it is your most powerful tool for catching bugs before your code ever runs. A well-typed C++ program carries its invariants in the type system itself, making entire classes of errors impossible.
The type of a variable determines three things: how much memory it occupies, what values it can represent, and what operations are valid on it. Getting types right is the foundation of correct C++ code.
Fundamental Types
C++ provides a set of built-in types that map directly to hardware capabilities. Their exact sizes vary by platform, but the standard guarantees minimum ranges:
#include <iostream>
#include <cstdint> // Fixed-width integer types
#include <limits> // std::numeric_limits
int main() {
// Boolean
bool is_valid = true; // true or false, typically 1 byte
// Character types
char letter = 'A'; // At least 8 bits, holds ASCII
char8_t u8ch = u8'A'; // C++20: exactly 8 bits, UTF-8
char16_t u16ch = u'\u00E9'; // At least 16 bits, UTF-16
char32_t u32ch = U'\U0001F600'; // At least 32 bits, UTF-32
// Integer types (sizes are minimums, typical x86-64 shown)
short s = 32000; // At least 16 bits (16)
int i = 2'000'000; // At least 16 bits (32) — digit separator C++14
long l = 100'000L; // At least 32 bits (64 on Linux, 32 on Windows)
long long ll = 9'000'000'000'000'000'000LL; // At least 64 bits
// Unsigned variants
unsigned int ui = 42u; // Cannot be negative
// Floating-point types
float f = 3.14f; // ~7 decimal digits precision
double d = 3.141592653589793; // ~15 decimal digits precision
long double ld = 3.14159265358979323846L;
// Fixed-width types from <cstdint> — use these when size matters
std::int32_t exact = 42; // Exactly 32 bits, guaranteed
std::uint64_t big = 18'446'744'073'709'551'615ULL; // Exactly 64 bits
std::int_fast32_t fast = 42; // At least 32 bits, fastest available
std::size_t index = 0; // Unsigned, suitable for sizes and indices
std::cout << "int: " << sizeof(int) << " bytes, max = "
<< std::numeric_limits<int>::max() << '\n';
}Initialization: The Right Way
C++ has evolved multiple initialization syntaxes. Uniform (brace) initialization {} is preferred in modern C++ because it prevents narrowing conversions:
#include <iostream>
#include <string>
int main() {
// Direct initialization
int a(42); // OK but can be confused with function declaration
// Copy initialization
int b = 42; // OK, familiar from C
// Uniform (brace) initialization — preferred in modern C++
int c{42}; // Direct-list-initialization
int d = {42}; // Copy-list-initialization
// Brace initialization PREVENTS narrowing conversions
// double to int loses precision — the compiler will ERROR:
// int narrow{3.14}; // ERROR: narrowing conversion
int ok = 3.14; // Compiles! Silently truncates to 3 (dangerous)
// Value initialization (zero for built-in types)
int zero{}; // Guaranteed to be 0
double dzero{}; // Guaranteed to be 0.0
bool bfalse{}; // Guaranteed to be false
// auto deduction
auto x = 42; // int
auto y = 3.14; // double
auto z = 'A'; // char
auto s = std::string{"hello"}; // std::string (not const char*!)
// decltype — queries the type of an expression
decltype(x) another = 100; // same type as x → int
decltype(auto) ref = (x); // int& because (x) is an lvalue expression
std::cout << "zero-initialized int: " << zero << '\n';
std::cout << "auto-deduced string: " << s << '\n';
}In C, the null pointer is represented by the macro NULL, which typically expands to 0 or (void*)0. This causes ambiguity in C++ because 0 is also a valid int. Consider a function overloaded for both int and a pointer type — passing NULL (which is 0) would call the int overload, not the pointer one.
C++11 introduced nullptr, a keyword of type std::nullptr_t that is implicitly convertible to any pointer type but not to integers. Always use nullptr instead of NULL or 0 for null pointers.
Similarly, prefer '\0' (the null character) when you mean the character with value zero, rather than 0, to communicate intent clearly.
- Always use nullptr instead of NULL or 0 for null pointers
- nullptr has its own type (std::nullptr_t) and avoids overload ambiguity
- NULL is a C macro that expands to 0 — it is an integer, not a pointer
- Use '\0' for the null character to distinguish it from the integer 0
const and Type Qualifiers
The const qualifier tells the compiler (and human readers) that a value must not be modified after initialization. It is one of the most important tools in C++ for writing safe, self-documenting code.
const is read right-to-left: const int* is a pointer to a const int (the int cannot be changed, the pointer can). int* const is a const pointer to int (the pointer cannot be changed, the int can). const int* const is a const pointer to a const int.
constexpr (C++11) goes further: it means the value must be computable at compile time. This enables the compiler to evaluate expressions during compilation, eliminating runtime cost entirely. Use constexpr for constants that are truly known at compile time.
consteval (C++20) is even stricter: it marks a function that must be evaluated at compile time — calling it at runtime is a compilation error.
The volatile qualifier tells the compiler that a variable's value may change outside the program's control (e.g., hardware registers, memory-mapped I/O). It prevents certain optimizations. Do not use volatile for threading — use std::atomic instead.
One of the most insidious bugs in C and old-style C++ comes from narrowing conversions — implicit conversions that lose information. For example, assigning a double to an int silently truncates the fractional part. Assigning a large int to a short silently overflows.
Brace initialization {} prevents narrowing conversions at compile time. This is one of the strongest reasons to prefer it over = initialization. If you write int x{3.14};, the compiler will emit an error. With int x = 3.14;, it silently truncates to 3.
Similarly, mixing signed and unsigned types in arithmetic causes implicit conversions that can produce surprising results. A negative int compared with an unsigned int will be converted to unsigned first, potentially becoming a very large positive number.
- Brace initialization {} catches narrowing conversions at compile time
- Assignment initialization (=) silently allows narrowing — data is lost without warning
- Mixing signed and unsigned types causes implicit conversion to unsigned
- A negative int converted to unsigned becomes a very large positive number
Type Aliases
Type aliases improve readability and make it easier to change types later. Prefer using over typedef in modern C++:
#include <cstdint>
#include <vector>
#include <string>
// Modern style (using) — preferred
using Byte = std::uint8_t;
using StudentId = std::int32_t;
using Names = std::vector<std::string>;
// Old style (typedef) — equivalent but harder to read with complex types
typedef std::uint8_t OldByte;
typedef std::vector<std::string> OldNames;
// using works with templates; typedef does not easily
template <typename T>
using Vec = std::vector<T>;
int main() {
Byte b{255};
StudentId sid{12345};
Names names{"Alice", "Bob", "Charlie"};
Vec<double> values{1.0, 2.0, 3.0};
}- Every C++ expression has a type known at compile time — the type determines size, valid values, and valid operations
- Use fixed-width types from
(int32_t, uint64_t) when the exact size matters - Prefer brace initialization {} — it prevents dangerous narrowing conversions
- Use nullptr (not NULL or 0) for null pointers to avoid overload ambiguity
- auto deduces the type from the initializer; decltype queries the type of an expression without evaluating it
- Use constexpr for compile-time constants and 'using' for type aliases
Quiz — Test Your Knowledge
(15 XP)1. What is the advantage of brace initialization `int x{42};` over assignment initialization `int x = 42;`?
2. Why should you use `nullptr` instead of `NULL` in C++?
3. What does `auto x = 42;` deduce the type of `x` to be?