AlgoMaster Logo

Type Traits

Low Priority14 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

Type traits are small templates in the <type_traits> header that answer questions about types at compile time. They report whether a type is an integer, whether it's a pointer, whether two types are the same, and they can transform one type into another, all without running any code. Modern C++ generics lean on them constantly: std::vector, std::sort, and std::optional all read type traits internally to pick the right implementation for whichever type is passed.

What Type Traits Are

A type trait is a template, usually a struct, whose specializations encode a fact about its type argument. The simplest ones expose a static value member that's either true or false. Others expose a nested type member that returns a transformed type.

std::is_integral<int>::value is a compile-time constant. The compiler resolves it during template instantiation, not at runtime, so it costs nothing in the generated binary. Reading the value member is reading a static constexpr bool.

The point of having these traits is to make generic code smarter about the types it sees. Consider a Cart<T> that holds items and should refuse template instantiations where T is something nonsensical like a function pointer or a void. Without type traits, there's no way to express that check in code. With them, it's one line.

Type trait queries are evaluated entirely at compile time. They add zero runtime work and no extra code to the binary. The only price is slightly slower compile times when many of them stack in deeply nested templates.

Primary Type Categories

Every C++ type belongs to exactly one primary category. The traits for these categories are the building blocks for almost everything else. The most common ones in everyday code:

TraitTrue when T is
std::is_integral<T>An integer type (int, long, char, bool, etc.)
std::is_floating_point<T>float, double, or long double
std::is_pointer<T>A raw pointer (T*), not including references or smart pointers
std::is_reference<T>An lvalue or rvalue reference (T& or T&&)
std::is_class<T>A class or struct type
std::is_enum<T>An enum (scoped or unscoped)
std::is_array<T>A C-style array (T[N]), not std::array
std::is_function<T>A function type (rare; usually pointers to functions)
std::is_void<T>The void type

A walkthrough showing several at once on e-commerce types.

A type belongs to exactly one primary category. int* is a pointer, not also an integral type. std::string is a class type, neither integral nor an array even though it can hold characters. The category is about the type itself, not what it represents.

This taxonomy comes straight from the C++ standard. std::is_pointer<T> doesn't trigger for std::unique_ptr<T> or std::shared_ptr<T>: those are class types, not raw pointers. For "any pointer-like thing", a custom trait is needed.

Composite Categories

The primary categories are mutually exclusive, but the standard also ships composite traits that bundle them. These are the ones most often used when constraining generic code.

TraitTrue when
std::is_arithmetic<T>T is integral or floating point
std::is_fundamental<T>Arithmetic, or void, or nullptr_t
std::is_object<T>Anything except references, functions, or void
std::is_scalar<T>Arithmetic, pointer, enum, member pointer, or nullptr_t
std::is_compound<T>Anything that's not fundamental

is_arithmetic is the most commonly used. It's how the standard library says "must be a number".

is_arithmetic is std::is_integral<T>::value || std::is_floating_point<T>::value packaged into one trait. Use the composite when the question is "is it any kind of number?". Use the primary trait only when the specific kind matters.

A worked example showing where each composite lights up.

References aren't "objects" in the type-trait sense. The standard calls something an "object type" when it can be stored, copied, and have its address taken. A reference can't be reassigned to refer to something else, so it's not an object type.

Type Properties

Beyond categories, traits can ask about properties layered on top of a type: is it const? Is it volatile? Is it signed? These are the second tier of common questions.

TraitTrue when
std::is_const<T>T is const-qualified
std::is_volatile<T>T is volatile-qualified
std::is_signed<T>T is a signed arithmetic type
std::is_unsigned<T>T is an unsigned arithmetic type
std::is_empty<T>T is a class with no non-static data members
std::is_polymorphic<T>T is a class with at least one virtual function
std::is_abstract<T>T is an abstract class (has a pure virtual function)
std::is_trivial<T>T is trivially constructible and copyable

A small demo:

One detail worth knowing: std::is_const<int* const> is true (the pointer is const), but std::is_const<const int*> is false (the pointee is const, not the pointer). Type traits look at the top level of the type only. Anything past a layer of indirection is its own type.

Supported Operations

Another family of traits asks "can this operation be performed with this type?". These are how libraries like std::vector decide between calling a move constructor and falling back to copy, or whether they can use an optimized memcpy path for a trivial type.

TraitTrue when
std::is_default_constructible<T>T() is well-formed
std::is_copy_constructible<T>T(const T&) is well-formed
std::is_move_constructible<T>T(T&&) is well-formed
std::is_copy_assignable<T>T& operator=(const T&) is well-formed
std::is_move_assignable<T>T& operator=(T&&) is well-formed
std::is_destructible<T>~T() is well-formed
std::is_invocable<F, Args...>F can be called with the given args
std::is_constructible<T, Args...>T(Args...) is well-formed

A practical e-commerce illustration. A Customer type is intentionally non-copyable because each customer must have a unique identity.

This is useful for generic code that must behave correctly for both copyable and move-only types. The standard library uses traits like these constantly. std::vector::push_back, for instance, calls move when is_nothrow_move_constructible is true and falls back to copy otherwise, because it can only guarantee the strong exception safety it promises if the move can't throw.

Querying constructibility traits forces the compiler to do real overload resolution as if it were trying to call the constructor. For a class with many overloads or complex template constructors, this isn't free. Heavy use in tight templates can slow compile times noticeably.

Type Relationships

Two-type traits ask how a pair of types relate. These are heavily used in template machinery for inheritance hierarchies and conversion logic.

TraitTrue when
std::is_same<T, U>T and U are exactly the same type
std::is_base_of<Base, Derived>Base is a base class of Derived
std::is_convertible<From, To>A value of From is implicitly convertible to To

A small illustration with an order hierarchy. Online orders inherit from a generic order.

std::is_same is strict. It treats int and const int as different types, and treats int and long as different even though they often share the same size. For "same up to const-ness", strip const first with std::remove_cv.

std::is_base_of reports true for a type compared to itself. That is, std::is_base_of<Order, Order>::value is true. The standard reads "is Base a base of Derived (or the same type)". Worth remembering.

Type Transformations

Until now, every trait has been a question with a true/false answer. The other half of <type_traits> is transformations: traits that take a type and return a related type. These are written as structs with a nested ::type member.

TransformationResult
std::remove_const<T>T without top-level const
std::remove_volatile<T>T without top-level volatile
std::remove_cv<T>T without const or volatile
std::remove_reference<T>T without & or &&
std::remove_pointer<T>What T points to, if T is a pointer
std::add_const<T>T with top-level const added
std::add_pointer<T>T*
std::decay<T>T after the conversions a function-by-value parameter would apply
std::conditional<B, T, F>T if B is true, else F

decay does a lot of work and deserves attention. When an argument is passed by value to a function, the compiler strips top-level references, strips top-level cv-qualifiers, decays arrays to pointers, and decays functions to function pointers. std::decay exposes that same set of transformations.

std::conditional is a compile-time ternary on types. It's how generic code picks between two type alternatives based on a condition.

The BestStorage alias picks T directly when T fits in a pointer-sized slot, and falls back to T* otherwise. Generic library code makes decisions like this routinely.

The pattern is consistent across all transformations. Feed in a type, read off ::type, get back the transformed result. Composing them allows building up sophisticated type calculations.

The _v and _t Helper Suffixes

typename std::is_integral<T>::value and typename std::remove_reference<T>::type are clunky to type. C++14 added _t aliases for transformations, and C++17 added _v variable templates for boolean traits, specifically to clean this up.

Old form (C++11)Modern form
std::is_integral<T>::valuestd::is_integral_v<T>
std::is_same<T, U>::valuestd::is_same_v<T, U>
typename std::remove_reference<T>::typestd::remove_reference_t<T>
typename std::decay<T>::typestd::decay_t<T>
typename std::conditional<B, T, F>::typestd::conditional_t<B, T, F>

The same logic, shorter to type and read.

Modern code uses _v and _t almost exclusively. The longer ::value and ::type forms still work everywhere, but they mostly appear in code older than C++17 or in places where the author is being deliberately portable to older compilers.

Brief: enable_if with Type Traits

std::enable_if is the bridge from "can a question be asked about a type?" to "can an overload be chosen based on the answer?". The short version of how traits feed into it.

The return type, std::enable_if_t<std::is_arithmetic_v<T>, T>, only resolves to T when is_arithmetic_v<T> is true. For any other type, the substitution fails and the overload is removed from the candidate set. Calling applyTaxRate("free") produces a clear compile error saying no matching function was found.

This pairing of "trait query" with "enable_if gating" was the dominant way to constrain templates before C++20.

Writing Custom Type Traits

Sometimes the standard doesn't have the needed trait. Custom traits use a two-template pattern: a primary template that inherits from std::false_type by default, and a specialization that inherits from std::true_type for the case being detected.

A custom trait that detects whether a type is a std::vector of anything:

std::true_type and std::false_type are shortcuts for "this trait's value is true/false". Inheriting from them provides the ::value member. The custom trait then works exactly like a standard library trait.

A second example: a trait that checks whether a type has a name member of type std::string. The detection pattern comes from the SFINAE lesson, applied here to a fresh case.

The mechanics here (the std::void_t trick, std::declval) come straight from the SFINAE lesson. The pattern is general: define a primary template that says "no", and a specialization that matches only when the expression being checked is well-formed.

When to Prefer C++20 Concepts

Concepts in C++20 take the same job that enable_if + type traits did and make it cleaner. Instead of std::enable_if_t<std::is_arithmetic_v<T>, T> in a return type, use template<std::integral T> or template<typename T> requires std::is_arithmetic_v<T>, and the constraint sits where it's expected.

The traits themselves haven't gone anywhere. std::is_integral, std::is_same, std::remove_reference, and the rest are still the workhorse mechanisms underneath. Concepts mostly change how they're applied at the function signature level. Type traits remain the only way to:

  • Query type properties inside the body of a template (with if constexpr).
  • Transform one type into another for use in nested type aliases.
  • Build the predicates that named concepts wrap.

For C++20 or newer, prefer concepts for constraining template parameters at the signature. Keep using type traits for everything else.

Quiz

Type Traits Quiz

10 quizzes