Type Traits
"Compile-time type inspection and transformation via <type_traits>: primary categories, property queries, type modifications, and custom trait authoring."
Type Traitssince C++11Compile-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:
// 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:
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 structCritical 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
// 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[]> // trueis_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
// 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> // intstd::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
// 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
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&&> // intConditional and Selection Traits
// 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> // longExamples
if constexpr dispatch (C++17)
#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
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)
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
// 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
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/_taliases everywhere (C++17). The::value/::typeforms add noise with no benefit. - Prefer
if constexproverenable_ifinside a function body (C++17).enable_ifbelongs at the function signature when you genuinely need to participate in overload resolution. - Prefer concepts over
enable_ifin C++20. Concept constraints report violations at the call site with the constraint that failed;enable_iffailures spiral into inscrutable substitution backtraces. - Propagate
noexceptwithis_nothrow_*traits.std::vectorand most container operations are only move-optimized when the element's move constructor isnoexcept. Not annotating your generic wrappers correctly silently degrades performance. - Use
static_assertat 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_twhen storing forwarded arguments. RawT&&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:
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:
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:
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 checkdecay_t on arrays produces a pointer, not what you want to store:
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).
// 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.
// 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.
// 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.
// 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.
// 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).
// 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);
}// 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 traitsstd::declvalβ manufacture a value of typeTin unevaluated contexts- SFINAE β the substitution failure mechanism underlying
enable_if if constexprβ compile-time branching without overload explosionstd::integral_constantβ the base template every trait is built on