C++ Type Traits for Compile-Time Introspection

The <type_traits> header in C++ provides a comprehensive suite of compile-time templates used to query, inspect, and evaluate the characteristics of types at compile time. Introduced in C++11 and expanded in subsequent standards (C++14, C++17, and C++20), these traits allow developers to inspect fundamental categories, examine structural properties, and evaluate relationships between types. This article breaks down the standard introspection traits into functional categories, highlighting how they enable metaprogramming, template constraints, and conditional compilation.

Fundamentals of Introspection Traits

Type introspection traits inherit from std::integral_constant<bool, Value>, meaning they expose a boolean compile-time constant member named value. Since C++17, all boolean type traits feature an inline variable template alias ending in _v (e.g., std::is_integral_v<T> instead of std::is_integral<T>::value), simplifying usage inside templates and static_assert statements.

Primary Type Categories

Primary type categories verify the fundamental nature of a type. Every C++ type belongs to exactly one primary category:

  • std::is_void<T>: Checks if T is void.
  • std::is_null_pointer<T>: Checks if T is std::nullptr_t (C++14).
  • std::is_integral<T>: Checks if T is an integral type (e.g., bool, char, int, long long).
  • std::is_floating_point<T>: Checks if T is a floating-point type (float, double, long double).
  • std::is_array<T>: Checks if T is an array type of known or unknown bound (e.g., int[] or int[5]).
  • std::is_pointer<T>: Checks if T is a raw pointer (excludes member pointers).
  • std::is_lvalue_reference<T>: Checks if T is an lvalue reference (U&).
  • std::is_rvalue_reference<T>: Checks if T is an rvalue reference (U&&).
  • std::is_member_object_pointer<T>: Checks if T points to a non-static data member.
  • std::is_member_function_pointer<T>: Checks if T points to a non-static member function.
  • std::is_enum<T>: Checks if T is an enumeration type (enum or enum class).
  • std::is_union<T>: Checks if T is a union type.
  • std::is_class<T>: Checks if T is a non-union class or struct.
  • std::is_function<T>: Checks if T is a function type.

Composite Type Categories

Composite categories are logical combinations of one or more primary categories:

  • std::is_reference<T>: Checks if T is either an lvalue or rvalue reference.
  • std::is_arithmetic<T>: Checks if T is either an integral or floating-point type.
  • std::is_fundamental<T>: Checks if T is void, arithmetic, or nullptr_t.
  • std::is_compound<T>: Checks if T is non-fundamental (arrays, functions, pointers, references, classes, unions, enums).
  • std::is_member_pointer<T>: Checks if T is a pointer to a member object or member function.
  • std::is_scalar<T>: Checks if T is arithmetic, enum, pointer, member pointer, or nullptr_t.
  • std::is_object<T>: Checks if T is not a function, reference, or void type.

Type Properties and Modifiers

These traits inspect the qualities, qualifiers, and memory characteristics of types:

Const-Volatile Qualifiers and Sign

  • std::is_const<T>: Checks if T is const-qualified.
  • std::is_volatile<T>: Checks if T is volatile-qualified.
  • std::is_signed<T>: Checks if T is a signed arithmetic type.
  • std::is_unsigned<T>: Checks if T is an unsigned arithmetic type.

Class Layout and Characteristics

  • std::is_empty<T>: Checks if T has no non-static data members (except zero-width bitfields) and no virtual functions.
  • std::is_polymorphic<T>: Checks if T defines or inherits at least one virtual function.
  • std::is_abstract<T>: Checks if T contains at least one pure virtual function.
  • std::is_final<T>: Checks if T is declared with the final specifier (C++14).
  • std::is_aggregate<T>: Checks if T is an aggregate type (C++17).
  • std::is_standard_layout<T>: Checks if T conforms to standard-layout rules for C-style interop.
  • std::is_trivial<T>: Checks if T is trivially copyable and has one or more trivial default constructors.

Supported Operations and Construction

The standard library provides inspection traits to verify how types can be constructed, assigned, and destroyed:

  • std::is_constructible<T, Args...>: Checks if T can be constructed with the argument types Args....
  • std::is_default_constructible<T>: Checks if T has an accessible default constructor.
  • std::is_copy_constructible<T>: Checks if T can be copy-constructed.
  • std::is_move_constructible<T>: Checks if T can be move-constructed.
  • std::is_assignable<T, U>: Checks if a type U can be assigned to T.
  • std::is_copy_assignable<T>: Checks if T can be copy-assigned.
  • std::is_move_assignable<T>: Checks if T can be move-assigned.
  • std::is_destructible<T>: Checks if T has an accessible, non-deleted destructor.

Variants of these traits exist with prefixes to enforce stricter safety guarantees:

  • std::is_trivially_*: Verifies that the operation requires no compiler-generated logic (e.g., standard bitwise copy operations).
  • std::is_nothrow_*: Verifies that the operation is guaranteed not to throw exceptions (noexcept).

Type Relationships and Invocability

These traits inspect the compatibility and relationship between two or more types:

  • std::is_same<T, U>: Checks if T and U are identical, taking const/volatile qualifiers into account.
  • std::is_base_of<Base, Derived>: Checks if Base is a base class of Derived (or the same class).
  • std::is_convertible<From, To>: Checks if a type From can be implicitly converted to To.
  • std::is_invocable<Fn, Args...>: Checks if a callable object Fn can be invoked with the arguments Args... (C++17).
  • std::is_invocable_r<R, Fn, Args...>: Checks if Fn can be invoked with Args... and yields a result convertible to R (C++17).

Practical Application

These introspection traits are predominantly used in conjunction with static_assert for compile-time validation, std::enable_if (or C++20 requires clauses) for template overload resolution, and if constexpr statements for branch pruning:

template <typename T>
void process_data(T value) {
    if constexpr (std::is_pointer_v<T>) {
        // Branch compiled only if T is a pointer
    } else if constexpr (std::is_integral_v<T>) {
        // Branch compiled only if T is an integer
    } else {
        static_assert(std::is_class_v<T>, "T must be a class, pointer, or integral type");
    }
}