Polyglot
Languages C++ types
C++ § types

Types

The C++ type system extends C’s substantially. The fundamental types are largely C’s; the additions are class types as user-defined first-class types, references as a distinct kind of indirection, scoped enumerations (enum class), auto and decltype for inferred types, structured bindings, type aliases through using, the constexpr/consteval/constinit qualifiers, and a rich library of type traits that lets templates inspect type properties at compile time. The system is statically checked, predominantly value-oriented, and designed to permit zero-overhead abstractions: a user-defined type with a constructor that performs the same work as initialising a fundamental type pays no runtime cost beyond what the equivalent C code would incur.

The standard organises the types into a hierarchy: fundamental types (arithmetic, void, nullptr_t), compound types (pointer, reference, array, function, class, union, enumeration, member-pointer), and the type modifiers that compose them. The arithmetic conversions, the rules for type compatibility, and the strict aliasing constraints all derive from this hierarchy.

Fundamental types

The fundamental types include the C arithmetic types plus bool, wchar_t, char8_t (C++20), char16_t, char32_t, and nullptr_t:

CategoryTypes
Booleanbool
Characterchar, signed char, unsigned char, wchar_t, char8_t, char16_t, char32_t
Integershort, int, long, long long (each in signed and unsigned forms)
Floating-pointfloat, double, long double
Voidvoid
Null pointerstd::nullptr_t

The exact widths of the integer types are implementation-defined; the standard guarantees minimum widths the same as C (int at least 16, long at least 32, long long at least 64). For specific widths, code uses the fixed-width aliases from <cstdint>:

#include <cstdint>

std::int32_t  exact = 1 << 30;
std::uint64_t big   = std::uint64_t{1} << 50;

bool is a distinct type that holds true or false; conversion from any scalar to bool yields false for zero and true otherwise. C++ also distinguishes char, signed char, and unsigned char as three separate types, even though char shares the representation of one of the latter two.

Compound types

Several mechanisms compose existing types into new ones.

Pointers and references

int  x  = 0;
int *p  = &x;        // pointer
int &r  = x;         // lvalue reference (must be initialised; cannot be rebound)
int &&rr = 42;       // rvalue reference (binds to temporaries; C++11)

References are distinct from pointers in syntax and semantics. A pointer may be reassigned, may be null, requires explicit dereferencing; a reference is bound at initialisation, cannot be null, and behaves like an alias for the bound object. The differences are treated in References and pointers.

Arrays

An array type T[N] denotes a contiguous sequence of N objects of type T. The mechanics are inherited from C: arrays decay to pointers in most expression contexts, the length is part of the type, multidimensional arrays are arrays of arrays, and the index is from zero.

int    a[10];                     // array of ten ints
double m[3][4];                   // 3×4 row-major
const char s[] = "hello";         // array of six chars

For most uses, the standard library’s std::array (C++11) is preferable: it is a thin wrapper around the C array but admits the usual iterator and member-function interface, and it does not decay to a pointer.

#include <array>

std::array<int, 10> a{};          // zero-initialised
a[0] = 42;

Classes and unions

The class types — declared with class, struct, or union — are user-defined types with members, constructors, destructors, and member functions. The differences between class and struct are purely cosmetic: members and bases default to public access in struct, private in class. Both are otherwise identical:

struct Point {
    double x = 0;
    double y = 0;
};

class Counter {
public:
    Counter();
    void increment();
    int  value() const;
private:
    int count_ = 0;
};

Union types hold at most one of their members at a time; all members occupy the same storage. Modern C++ rarely uses raw unions; the safer alternative std::variant (C++17) carries a discriminator.

The full treatment of classes is in Classes and OOP.

Enumerations

Two enumeration kinds:

  • Unscoped (enum) — the enumerators are visible in the enclosing scope; the type implicitly converts to int.
  • Scoped (enum class, C++11) — the enumerators are accessed only through the type name; no implicit conversion to integer.
enum Direction       { NORTH, EAST, SOUTH, WEST };       // unscoped
enum class State     { Idle, Running, Stopped };         // scoped (C++11)
enum class Code : int { OK = 0, Error = 1, Timeout = 2 }; // explicit underlying type

Direction d = SOUTH;        // unqualified
State     s = State::Idle;  // qualified

int       i = SOUTH;        // legal: implicit conversion
int       j = static_cast<int>(s);   // explicit conversion required

Scoped enumerations are the conventional choice in modern C++; the implicit-integer conversion of the unscoped form is rarely desirable.

Function types

int(int, double) is the type of a function taking an int and a double and returning an int. Function types appear most commonly as the argument-type to a pointer or reference, in std::function, in template parameters, and in trailing-return-type forms:

int  f(int);                          // function declaration
int  (*fp)(int) = f;                  // pointer to function
auto fr = f;                          // also a function pointer (deduced)
auto g(int x) -> int { return x; }    // trailing return type

Member pointers

A pointer to a member of a class type is a separate kind from an ordinary pointer:

struct Point { double x, y; };

double Point::*member = &Point::x;     // pointer to member

Point p{3.0, 4.0};
double v = p.*member;                  // 3.0

Member pointers are rare in routine code but central to certain library facilities (std::bind, generic data-binding helpers).

Type aliases

Two mechanisms introduce aliases for existing types:

typedef unsigned long size_t;          // C-style
using   size_t = unsigned long;        // C++11; preferred

using Comparator = bool(*)(int, int);  // function-pointer alias
template <typename T>
using Vec = std::vector<T>;            // template alias (C++11)

The using form is preferred in modern C++: it admits template aliases, which typedef does not.

auto and decltype

Two type-deduction facilities, both introduced in C++11.

auto deduces a type from an initialiser:

auto x = 42;            // int
auto y = 3.14;          // double
auto v = std::vector<int>{1, 2, 3};  // std::vector<int>
auto p = std::make_unique<Widget>();  // std::unique_ptr<Widget>

auto strips top-level const and reference qualifiers; auto&, const auto&, and auto* preserve them:

const std::vector<int> v = {…};
auto         x = v[0];     // int (no const, no reference)
const auto&  y = v[0];     // const int& (no copy)

The deduction rules are those of template argument deduction; the special form auto&& produces a forwarding reference that binds appropriately to lvalues and rvalues.

decltype yields the type of an expression without evaluating it:

int  x = 0;
decltype(x)         a = 0;       // int
decltype(x + 1)     b = 0;       // int
decltype((x))       c = x;       // int& — note the parentheses

The construct is principally useful in templates and in trailing return types where the return type depends on the parameter types.

Structured bindings

C++17 introduced structured bindings, which decompose tuples, pairs, arrays, and class types with public members into named variables:

std::map<std::string, int> ages = {{"alice", 30}, {"bob", 28}};
for (const auto &[name, age] : ages) {
    std::cout << name << ": " << age << '\n';
}

std::pair<int, double> p{1, 2.5};
auto [n, x] = p;        // n is int, x is double

The construction is the conventional way to unpack the elements of a return-by-tuple function, the entries of a map, or the members of a small aggregate.

Type qualifiers

The qualifiers const, volatile, and the compile-time qualifiers (constexpr, consteval, constinit) modify existing types. The combinations follow the C rules; const may apply to any level of a derived type and to member functions of a class:

const int           ci = 42;
const int *         cip = &ci;       // pointer to const int
int * const         ipc = &i;        // const pointer to int
const int * const   cipc = &ci;      // const pointer to const int

class Widget {
    int  value() const;              // member function: does not modify *this
    void set(int v);                 // non-const member function
};

The const member function is a substantive type-level distinction: a const Widget may invoke only const member functions on itself.

Conversions

Implicit conversions occur in arithmetic, assignment, and function-argument passing. Two rules govern the arithmetic case.

The integer promotions

In any expression where an integer of a type narrower than int appears as an operand, the value is promoted to int (or unsigned int if int cannot represent every value of the original type). The promotions apply to char, signed char, unsigned char, short, unsigned short, the bit-field types, and bool.

The usual arithmetic conversions

When two operands of arithmetic type appear in a binary expression, both are converted to a common type — the type of higher rank. The rules are essentially C’s; the principal practical consequence is that mixing signed and unsigned in the same expression converts the signed to unsigned, which can produce surprising results:

int    i = -1;
size_t n = 5;
if (i < n) /* never executed: i is converted to size_t */;

Explicit conversions: the four named casts

C++ provides four named cast expressions that supersede C’s parenthesised cast syntax:

CastPurpose
static_cast<T>(e)A conversion that the type system can verify at compile time. The conventional cast: numeric conversions, conversions up an inheritance hierarchy, conversions from void * to a concrete pointer, deliberate truncations.
dynamic_cast<T>(e)A runtime-checked cast across an inheritance hierarchy. Returns nullptr (for pointers) or throws std::bad_cast (for references) on failure. Requires polymorphic types (a virtual function in the source).
reinterpret_cast<T>(e)A bit-pattern reinterpretation. Conversion between unrelated pointer types, between integers and pointers, and similar low-level operations. Most uses are undefined behaviour without careful analysis.
const_cast<T>(e)Removes (or adds) const or volatile. Modifying an originally-const object through a const_cast-ed pointer is undefined.
int n = static_cast<int>(3.14);          // 3

Base   *b = new Derived();
Derived *d = dynamic_cast<Derived*>(b);  // checked downcast; nullptr if fails

uintptr_t addr = reinterpret_cast<uintptr_t>(p);

void take(int *p);
void take_const(const int *p);
const int  ci = 0;
take(const_cast<int*>(&ci));             // legal cast; modification is undefined

The discipline is to use static_cast for the conventional cases, dynamic_cast for polymorphic descents, and to avoid reinterpret_cast and const_cast outside of carefully-justified low-level code.

C-style casts ((int)x) remain legal and behave as the most-permissive interpretation of the four named casts. Modern C++ avoids them: the named casts make intent explicit and refuse the more dangerous conversions.

Type traits

The <type_traits> header provides a library of compile-time type predicates and transformations:

#include <type_traits>

static_assert(std::is_integral<int>::value);
static_assert(std::is_floating_point_v<double>);     // C++17 short form

template <typename T>
T add(T a, T b) requires std::is_arithmetic_v<T> {
    return a + b;
}

using clean = std::remove_const_t<std::remove_reference_t<const int&>>;  // int

The header is the foundation on which generic libraries reflect on their template parameters. Most type traits have a _v variant (a constexpr bool) and a _t variant (a type alias) introduced in C++14 / C++17; the _v and _t forms are preferred over the older ::value and ::type accesses.

Object representation

Every object occupies a contiguous region of bytes; sizeof(T) returns its size. The standard guarantees sizeof(char) == 1 but the number of bits in a char is CHAR_BIT, which is at least 8 and on every contemporary platform exactly 8.

Alignment

Each type has an alignment requirement: an address constraint that the standard specifies. alignof(T) returns the requirement; alignas(N) overrides it. The C alignment rules apply, with the addition that classes have alignment at least equal to that of their most-strictly-aligned member.

The strict aliasing rule

C++ inherits and tightens C’s strict aliasing rule: an object’s stored value may be accessed only through an lvalue of certain compatible types. Reinterpreting the bytes of one type as another requires std::bit_cast (C++20), memcpy, or carefully-managed unions:

#include <bit>
#include <cstdint>

float bits_to_float(std::uint32_t bits) {
    return std::bit_cast<float>(bits);     // since C++20: well-defined
}

std::bit_cast is the modern, type-safe, constexpr-friendly alternative to type-punning through pointer casts.

Type compatibility and the One Definition Rule

Two types are the same if they share the same name and, for class types, the same definition. The compiler does not match struct definitions across translation units; the One Definition Rule requires that, if a class is defined in two translation units, the definitions must be identical (same members, same order, same access, and so on). Violations are typically silent at compile time and produce undefined behaviour at runtime.

The conventional discipline:

  1. Each class is defined once, in a header.
  2. Every translation unit that uses the class includes the header.
  3. The same header is included by both the implementation file and any consumer.

The discipline is the same as C’s, with the addition that templates further complicate things — see Templates — and modules (C++20) provide a more robust alternative to inclusion.