Skip to content
C++
Library
since C++11
Intermediate

Type Traits

"Compile-time type inspection and transformation via <type_traits>: primary categories, property queries, type modifications, and custom trait authoring."

Type Traitssince C++11

Compile-time predicates and type transformations, defined in <type_traits>, that expose information about types and produce modified types with zero runtime overhead.

Overview

Every type trait is a template struct that exposes a ::value constant member (a bool or integer) or a ::type member alias. They all inherit from std::integral_constant<bool, V>, which gives them ::value, operator bool(), and operator(). The two common basesβ€”std::true_type and std::false_typeβ€”are integral_constant<bool, true/false> specializations.

C++17 added _v variable templates and _t type aliases for every standard trait, eliminating the verbose ::value and ::type suffixes:

cpp
// C++11 β€” the original form
static_assert(std::is_integral<int>::value);
using T = std::remove_const<const int>::type;

// C++17 β€” prefer these in all new code
static_assert(std::is_integral_v<int>);
using T = std::remove_const_t<const int>;

Include <type_traits> for all traits. No other headers required.


Primary Type Categories

Every type belongs to exactly one primary category. The set is exhaustive and disjoint:

cpp
std::is_void_v<void>                              // true
std::is_null_pointer_v<std::nullptr_t>            // true  (C++14 addition)
std::is_integral_v<int>                           // true β€” bool, char, wchar_t, char8_t (C++20), intN_t, ...
std::is_floating_point_v<double>                  // true β€” float, double, long double
std::is_array_v<int[5]>                           // true β€” includes T[] unbounded arrays
std::is_pointer_v<int*>                           // true β€” nullptr_t is NOT a pointer
std::is_lvalue_reference_v<int&>                  // true
std::is_rvalue_reference_v<int&&>                 // true
std::is_member_object_pointer_v<int Widget::*>    // true
std::is_member_function_pointer_v<void(Cls::*)()> // true
std::is_enum_v<Color>                             // true β€” scoped and unscoped enums
std::is_union_v<U>                                // true β€” only if U is a union
std::is_class_v<Widget>                           // true β€” struct and class, never union
std::is_function_v<void(int)>                     // true β€” bare function type, NOT pointer

// C++23
std::is_scoped_enum_v<Color>                      // true only for enum class / enum struct

Critical distinction: is_function_v<int(*)(int)> is false. A function pointer is a pointer; is_function_v matches bare function types like int(int), which arise in template contexts and std::function specializations, not as variable types.

Non-obvious: is_integral_v<bool> is true. bool is an integral type by the standard's definition.


Composite Categories and Property Queries

cpp
// Composite (logical OR of primaries)
std::is_reference_v<int&>               // true: lvalue or rvalue ref
std::is_arithmetic_v<int>               // true: integral || floating_point
std::is_fundamental_v<double>           // true: arithmetic || void || nullptr_t
std::is_scalar_v<int*>                  // true: arithmetic || pointer || enum || member_ptr || nullptr_t
std::is_object_v<int>                   // true: not function, not reference, not void
std::is_compound_v<int*>                // true: not fundamental

// cv-qualifiers β€” top-level type only
std::is_const_v<const int>             // true
std::is_const_v<const int*>            // false β€” the pointer itself is not const
std::is_const_v<int* const>            // true  β€” the pointer IS const
std::is_volatile_v<volatile int>       // true

// Signedness
std::is_signed_v<int>                  // true; is_signed_v<float> is ALSO true
std::is_unsigned_v<unsigned int>       // true; is_unsigned_v<bool> is true

// Array dimensions
std::rank_v<int[3][4]>                 // 2 (number of dimensions)
std::extent_v<int[3][4], 0>            // 3 (first dimension)
std::extent_v<int[3][4], 1>            // 4

// C++20: bounded vs unbounded arrays
std::is_bounded_array_v<int[5]>        // true
std::is_unbounded_array_v<int[]>       // true

is_signed_v tests whether T(-1) < T(0), so it is true for all floating-point types. When you mean "signed integer," gate on is_integral_v<T> && is_signed_v<T>.


Type Modifications

cpp
// Strip qualifiers
std::remove_const_t<const int>          // int
std::remove_cv_t<const volatile int>    // int
std::remove_reference_t<int&&>          // int
std::remove_cvref_t<const int&>         // int   (C++20; replaces remove_cv_t<remove_reference_t<T>>)
std::remove_pointer_t<int*>             // int

// Add qualifiers
std::add_const_t<int>                   // const int
std::add_lvalue_reference_t<int>        // int&
std::add_rvalue_reference_t<int>        // int&&
std::add_pointer_t<int>                 // int*

// decay: models pass-by-value argument rules
std::decay_t<int[3]>                    // int*          (array-to-pointer)
std::decay_t<void(int)>                 // void(*)(int)  (function-to-pointer)
std::decay_t<const int&>               // int           (strip cv-ref)

// Signed/unsigned
std::make_unsigned_t<int>               // unsigned int
std::make_signed_t<unsigned char>       // signed char

// Enum underlying type
std::underlying_type_t<Color>           // int, or whatever was specified

// C++17: always void if T is a valid type expression (detection idiom helper)
std::void_t<typename T::value_type>    // void, or substitution failure

// C++20: identity transformation β€” useful to inhibit argument deduction
std::type_identity_t<int>              // int

std::decay_t mirrors the implicit conversion rules applied when passing arguments by value. Use it when storing a type that was deduced from a forwarding reference, or when comparing a deduced type against a known type.


Special Member Functions and Layout Traits

cpp
// Constructibility
std::is_default_constructible_v<T>
std::is_constructible_v<T, int, double>       // constructible from (int, double)
std::is_nothrow_constructible_v<T, Args...>
std::is_trivially_default_constructible_v<T>

// Copy / move
std::is_copy_constructible_v<T>
std::is_nothrow_copy_constructible_v<T>
std::is_trivially_copy_constructible_v<T>
std::is_copy_assignable_v<T>
std::is_move_constructible_v<T>
std::is_nothrow_move_constructible_v<T>
std::is_move_assignable_v<T>
std::is_nothrow_move_assignable_v<T>

// Destructor
std::is_destructible_v<T>
std::is_trivially_destructible_v<T>
std::is_nothrow_destructible_v<T>

// Layout and ABI
std::is_trivially_copyable_v<T>       // safe for memcpy / memmove
std::is_trivial_v<T>                  // trivially constructible + trivially copyable
std::is_standard_layout_v<T>         // compatible with C struct layout rules
std::is_aggregate_v<T>               // aggregate-initializable  (C++17)
std::is_empty_v<T>                   // no non-static data; enables EBO
std::is_abstract_v<T>                // has at least one pure virtual function
std::is_polymorphic_v<T>             // has at least one virtual function
std::is_final_v<T>                   // declared final  (C++14)

is_trivially_copyable and is_standard_layout are independent. A type may be trivially copyable but have non-standard layout (mixed public/private data members). Use is_trivially_copyable for memcpy safety; use is_standard_layout for C ABI interop.


Type Relationships and Invocability

cpp
std::is_same_v<int, int>                     // true
std::is_same_v<int, const int>              // false β€” cv matters

std::is_base_of_v<Base, Derived>            // true; also true if Base == Derived
std::is_convertible_v<Derived*, Base*>      // true (implicit conversion)
std::is_nothrow_convertible_v<From, To>     // C++20

// Invocability (C++17)
std::is_invocable_v<F, Args...>
std::is_invocable_r_v<R, F, Args...>        // callable and return type convertible to R
std::is_nothrow_invocable_v<F, Args...>
std::invoke_result_t<F, Args...>            // deduced return type

// C++20
std::is_layout_compatible_v<T, U>          // same memory layout; relevant for union type-punning
std::common_reference_t<int&, int&&>       // int

Conditional and Selection Traits

cpp
// Select between two types
std::conditional_t<std::is_signed_v<T>, int64_t, uint64_t>

// SFINAE gate: produces the type or hard failure (enables overload set narrowing)
std::enable_if_t<std::is_integral_v<T>, T>

// C++20: prevents argument deduction; forces the caller to be explicit
void f(std::type_identity_t<int> x);   // deduction disabled for x

// Common arithmetic promotion type
std::common_type_t<int, double>         // double
std::common_type_t<int, long>           // long

Examples

if constexpr dispatch (C++17)

cpp
#include <type_traits>
#include <cstring>

template<typename T>
void serialize(const T& val, unsigned char* buf) {
    if constexpr (std::is_trivially_copyable_v<T>) {
        // No vtable, no heap indirection β€” safe to copy byte-by-byte
        std::memcpy(buf, &val, sizeof(T));
    } else {
        val.serialize(buf);   // T provides its own serializer
    }
}

Propagating noexcept with type traits

cpp
template<typename T>
class ring_buffer {
    alignas(T) unsigned char storage_[sizeof(T) * Capacity];
public:
    // Strong exception safety: noexcept iff T's move ctor is noexcept
    void push(T val) noexcept(std::is_nothrow_move_constructible_v<T>) {
        new (next_slot()) T(std::move(val));
    }
};

This pattern is used throughout the standard library itself β€” std::vector reallocation only moves elements if is_nothrow_move_constructible_v<T> is true, falling back to copy otherwise.

Detection idiom with void_t (C++17)

cpp
template<typename T, typename = void>
struct has_reserve : std::false_type {};

template<typename T>
struct has_reserve<T, std::void_t<
    decltype(std::declval<T&>().reserve(std::declval<std::size_t>()))
>> : std::true_type {};

template<typename T>
inline constexpr bool has_reserve_v = has_reserve<T>::value;

template<typename Container>
void fill_n(Container& c, std::size_t n) {
    if constexpr (has_reserve_v<Container>)
        c.reserve(n);           // avoid repeated reallocations
    for (std::size_t i = 0; i < n; ++i)
        c.push_back(typename Container::value_type{});
}

enable_if SFINAE vs C++20 concepts

cpp
// Pre-C++20: SFINAE via enable_if β€” hard to read, error messages are hostile
template<typename T>
std::enable_if_t<std::is_integral_v<T>, T>
checked_add(T a, T b) {
    if (a > 0 && b > std::numeric_limits<T>::max() - a)
        throw std::overflow_error("integer overflow");
    return a + b;
}

// C++20: concept constraint β€” cleaner syntax, far better diagnostics
template<std::integral T>
T checked_add(T a, T b) {
    if (a > 0 && b > std::numeric_limits<T>::max() - a)
        throw std::overflow_error("integer overflow");
    return a + b;
}

Specialization-based custom trait

cpp
template<typename T>
struct is_smart_pointer : std::false_type {};

template<typename T, typename D>
struct is_smart_pointer<std::unique_ptr<T, D>> : std::true_type {};

template<typename T>
struct is_smart_pointer<std::shared_ptr<T>> : std::true_type {};

template<typename T>
struct is_smart_pointer<std::weak_ptr<T>> : std::true_type {};

template<typename T>
inline constexpr bool is_smart_pointer_v = is_smart_pointer<T>::value;

static_assert( is_smart_pointer_v<std::unique_ptr<int>>);
static_assert(!is_smart_pointer_v<int*>);

Best Practices

  • Use _v/_t aliases everywhere (C++17). The ::value/::type forms add noise with no benefit.
  • Prefer if constexpr over enable_if inside a function body (C++17). enable_if belongs at the function signature when you genuinely need to participate in overload resolution.
  • Prefer concepts over enable_if in C++20. Concept constraints report violations at the call site with the constraint that failed; enable_if failures spiral into inscrutable substitution backtraces.
  • Propagate noexcept with is_nothrow_* traits. std::vector and most container operations are only move-optimized when the element's move constructor is noexcept. Not annotating your generic wrappers correctly silently degrades performance.
  • Use static_assert at the top of a template body for preconditions: static_assert(std::is_arithmetic_v<T>, "T must be arithmetic"). This produces a readable error at the call site instead of a page of instantiation noise.
  • Use decay_t when storing forwarded arguments. Raw T&& may decay to an array or function type that cannot be stored.
  • Put detection traits in headers. Two translation units with different definitions of the same void_t-detected trait can produce silent ODR violations.

Common Pitfalls

is_const checks the top-level type, not what a pointer points to:

cpp
static_assert(!std::is_const_v<const int*>);  // pointer is not const
static_assert( std::is_const_v<int* const>);  // pointer IS const
// To inspect the pointee: is_const_v<std::remove_pointer_t<const int*>>

is_signed is true for floating-point types:

cpp
static_assert(std::is_signed_v<float>);   // true β€” signed means T(-1) < T(0)
// Correct idiom for "signed integer": is_integral_v<T> && is_signed_v<T>

is_base_of is reflexive and ignores access:

cpp
std::is_base_of_v<Foo, Foo>                // true β€” a class is its own base
std::is_base_of_v<PrivateBase, Derived>    // true even for private inheritance
// is_convertible_v respects access; use it when you need that check

decay_t on arrays produces a pointer, not what you want to store:

cpp
template<typename T>
void bad_store(T&& v) {
    T local = v;              // T could be int[5] β€” not default-constructible!
}
template<typename T>
void good_store(T&& v) {
    std::decay_t<T> local = v;  // int* β€” always a value type
}

is_trivially_copyable does not imply is_standard_layout:
A struct with mixed public/private data members may still be trivially copyableβ€”memcpy is safeβ€”but its layout is not guaranteed to match a C struct. Distinguish the use cases and check both traits when both properties are required.


std::conditional_t and std::enable_if_t β€” Compile-Time Type Selection

std::conditional_t is the ternary operator for types. It resolves to one of two types depending on a compile-time boolean, and unlike enable_if it never produces a substitution failure β€” both branches must be well-formed types (their members are not inspected unless selected).

cpp
// C++17
#include <type_traits>
#include <cstdint>

// Choose the storage type based on required size
template<std::size_t N>
using int_for_size_t = std::conditional_t<(N <= 32), int32_t, int64_t>;

static_assert(std::is_same_v<int_for_size_t<16>, int32_t>);
static_assert(std::is_same_v<int_for_size_t<48>, int64_t>);

std::enable_if_t gates an overload or specialization on a trait. When the condition is false, substitution fails silently and the candidate is removed from the overload set β€” this is the SFINAE mechanism. There are three idiomatic placement sites: the return type, a defaulted template parameter, and a defaulted function parameter.

cpp
// C++11 β€” three equivalent ways to constrain a function template

// 1. Return-type gate (cleanest for single-overload functions)
template<typename T>
std::enable_if_t<std::is_integral_v<T>, T>
abs_val(T x) { return x < 0 ? -x : x; }

// 2. Extra defaulted template parameter (preferred when return type is complex)
template<typename T,
         typename = std::enable_if_t<std::is_floating_point_v<T>>>
T abs_val(T x) { return x < 0 ? -x : x; }

// 3. Defaulted non-type parameter (avoids "redefinition of default argument" ODR issue)
template<typename T>
T abs_val(T x, std::enable_if_t<std::is_arithmetic_v<T>>* = nullptr) {
    return x < 0 ? -x : x;
}

Placement 2 has a subtle defect: two overloads that differ only in the = std::enable_if_t<…> default argument are actually the same template with two conflicting defaults, causing a hard error. Placements 1 and 3 avoid this. In C++20, all three patterns are superseded by concept constraints.


std::invoke_result_t and std::is_invocable β€” Querying Callable Signatures

std::is_invocable determines at compile time whether a callable can be invoked with a given argument list. It handles all callable entities uniformly β€” free functions, member function pointers via std::invoke, lambdas, and function objects β€” following the same rules as std::invoke itself.

cpp
// C++17
#include <type_traits>
#include <functional>

struct Widget {
    int compute(double) const noexcept;
};

// Member function pointer β€” std::invoke semantics
static_assert(std::is_invocable_v<decltype(&Widget::compute), Widget, double>);
static_assert(!std::is_invocable_v<decltype(&Widget::compute), Widget, std::string>);

// Constrain a higher-order function
template<typename F, typename... Args>
std::enable_if_t<std::is_invocable_v<F, Args...>>
call_if_valid(F&& f, Args&&... args) {
    std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
}

std::invoke_result_t deduces the exact return type produced by a callable, without calling it. It replaces the older std::result_of_t (deprecated in C++17, removed in C++20) and is safe to use in SFINAE contexts because it participates in substitution failure rather than producing a hard error.

cpp
// C++17
#include <type_traits>
#include <vector>
#include <algorithm>

// Deduce the element type a transformation produces
template<typename Container, typename F>
auto transform_to_vec(const Container& c, F&& f) {
    using result_t = std::invoke_result_t<F, typename Container::value_type>;
    std::vector<result_t> out;
    out.reserve(c.size());
    for (const auto& v : c)
        out.push_back(std::invoke(std::forward<F>(f), v));
    return out;
}

Use std::is_invocable_r_v<R, F, Args...> when you also need to verify the return type is convertible to R, and std::is_nothrow_invocable_v to propagate noexcept through a generic callback wrapper.


C++20 Type Trait Additions

C++20 plugged several ergonomic gaps in <type_traits>. The additions below are thin but frequently needed; using them avoids hand-rolling equivalent expressions and makes intent explicit.

cpp
// C++20
#include <type_traits>

// remove_cvref_t β€” strips both cv-qualifiers and reference in one step
// Replaces the verbose remove_cv_t<remove_reference_t<T>> idiom
using T1 = std::remove_cvref_t<const volatile int&&>;  // int
using T2 = std::remove_cvref_t<const int&>;            // int
using T3 = std::remove_cvref_t<int>;                   // int (no-op)

std::remove_cvref_t is what you want when stripping all decoration from a forwarding-reference deduced type before comparing it with a known type (e.g., checking whether T is std::string regardless of how it was passed).

cpp
// C++20
// type_identity_t β€” inhibits template argument deduction for a parameter
template<typename T>
void set_all(std::vector<T>& v, std::type_identity_t<T> value) {
    // Without type_identity_t, passing {1,2} could make deduction ambiguous.
    // Now only the first parameter participates in deduction; value must
    // be implicitly convertible to T, not deduce an independent T.
    std::fill(v.begin(), v.end(), value);
}
cpp
// C++20
// is_bounded_array / is_unbounded_array β€” distinguish array kinds
static_assert( std::is_bounded_array_v<int[5]>);    // extent known
static_assert(!std::is_bounded_array_v<int[]>);     // no extent

static_assert( std::is_unbounded_array_v<int[]>);
static_assert(!std::is_unbounded_array_v<int[5]>);

// is_nothrow_convertible β€” conversion that doesn't throw
static_assert(std::is_nothrow_convertible_v<int, double>);   // true
static_assert(std::is_nothrow_convertible_v<int*, void*>);   // true

// Useful for asserting safe implicit-conversion paths in generic code:
template<typename From, typename To>
constexpr To safe_implicit_cast(From v) {
    static_assert(std::is_nothrow_convertible_v<From, To>,
                  "implicit conversion may throw; use an explicit cast");
    return v;
}

is_bounded_array and is_unbounded_array were added alongside the C++20 CTAD improvements to std::span; they let generic code distinguish T[N] (stack array with compile-time extent) from T[] (incomplete array type used in flexible array patterns) without relying on extent_v returning zero for the unbounded case.


See Also

  • <concepts> β€” C++20 named constraints built atop type traits
  • std::declval β€” manufacture a value of type T in unevaluated contexts
  • SFINAE β€” the substitution failure mechanism underlying enable_if
  • if constexpr β€” compile-time branching without overload explosion
  • std::integral_constant β€” the base template every trait is built on