Type traits in C++ are compile-time templates that answer questions about types or transform types. Defined in <type_traits>, they take a type as a template argument and expose information through a ::value member (true/false) or a ::type member (a transformed type). For example, std::is_integral<int>::value is true, std::is_pointer<int*>::value is true, and std::remove_const<const int>::type is int. Type traits are the foundation of if constexpr branches, SFINAE constraints, and Concepts.
Introduction
Templates in C++ are powerful — they let you write one function or class that works with many types. But sometimes “many types” is not quite right. A function that adds two numbers works for int, double, and float, but not for string or vector. A serialization function needs to handle integers differently from floating-point types, pointers differently from arrays, and user-defined types differently from all of them.
Before C++11, handling these distinctions required complex SFINAE tricks or separate template specializations for every case. C++11 standardized a comprehensive library of type traits in <type_traits> — a toolkit of compile-time predicates and type transformations that let you query and manipulate types at compile time with a clean, consistent interface.
Type traits serve four main purposes. Query traits answer yes/no questions about a type: Is it an integer? Is it a pointer? Does it have a virtual destructor? Property traits expose numeric properties: What is its alignment? How many array dimensions does it have? Transformation traits produce modified types: Remove the const. Add a pointer. Decay the type (remove cv-qualifiers and array/function decay). Relationship traits compare two types: Are they the same type? Is one derived from the other? Is one convertible to the other?
This article teaches type traits from first principles. You will understand the mechanics of how they work (template specialization), how to use the standard library traits effectively, how to write your own custom traits, and how traits combine with if constexpr, SFINAE, and Concepts to control template behavior.
The Mechanics: How Type Traits Work
Type traits are just templates with specializations. Understanding their implementation demystifies them completely.
#include <iostream>
#include <type_traits>
using namespace std;
// How is_pointer is actually implemented:
// Primary template: assume false
template<typename T>
struct my_is_pointer {
static constexpr bool value = false;
using type = false_type;
};
// Specialization for T* — overrides the primary template
template<typename T>
struct my_is_pointer<T*> {
static constexpr bool value = true;
using type = true_type;
};
// Specialization for const T* as well
template<typename T>
struct my_is_pointer<const T*> {
static constexpr bool value = true;
using type = true_type;
};
// How is_const is implemented:
template<typename T>
struct my_is_const : false_type {}; // Inheriting from false_type is idiomatic
template<typename T>
struct my_is_const<const T> : true_type {}; // Specialization for const T
// How remove_const is implemented:
template<typename T>
struct my_remove_const { using type = T; }; // Primary: return T unchanged
template<typename T>
struct my_remove_const<const T> { using type = T; }; // Specialization: strip const
// Convenience alias (C++14 style)
template<typename T>
using my_remove_const_t = typename my_remove_const<T>::type;
int main() {
// Our custom traits
cout << "=== Custom trait demos ===" << endl;
cout << "my_is_pointer<int>: " << my_is_pointer<int>::value << endl;
cout << "my_is_pointer<int*>: " << my_is_pointer<int*>::value << endl;
cout << "my_is_pointer<const int*>: " << my_is_pointer<const int*>::value << endl;
cout << "my_is_const<int>: " << my_is_const<int>::value << endl;
cout << "my_is_const<const int>: " << my_is_const<const int>::value << endl;
// remove_const in action
using T1 = my_remove_const_t<const double>; // double
using T2 = my_remove_const_t<double>; // double (unchanged)
cout << "remove_const<const double> == double? "
<< is_same<T1, double>::value << endl;
cout << "remove_const<double> == double? "
<< is_same<T2, double>::value << endl;
// The standard library versions
cout << "\n=== Standard <type_traits> ===" << endl;
cout << "is_pointer<int>: " << is_pointer<int>::value << endl;
cout << "is_pointer<int*>: " << is_pointer<int*>::value << endl;
cout << "is_const<const int>: " << is_const<const int>::value << endl;
cout << "is_same<int, int>: " << is_same<int, int>::value << endl;
cout << "is_same<int, double>: " << is_same<int, double>::value << endl;
// C++17 variable templates (trailing _v) — preferred in modern code
cout << "\n=== C++17 _v shortcuts ===" << endl;
cout << "is_pointer_v<int*>: " << is_pointer_v<int*> << endl;
cout << "is_const_v<const float>: " << is_const_v<const float> << endl;
cout << "is_same_v<int, int>: " << is_same_v<int, int> << endl;
// C++14 _t shortcuts for type transformation traits
cout << "\n=== C++14 _t shortcuts ===" << endl;
using NoCv = remove_cv_t<const volatile int>; // int
using NoRef = remove_reference_t<int&>; // int
cout << "remove_cv_t<const volatile int> == int? "
<< is_same_v<NoCv, int> << endl;
cout << "remove_reference_t<int&> == int? "
<< is_same_v<NoRef, int> << endl;
return 0;
}
Output:
=== Custom trait demos ===
my_is_pointer<int>: 0
my_is_pointer<int*>: 1
my_is_pointer<const int*>: 1
my_is_const<int>: 0
my_is_const<const int>: 1
remove_const<const double> == double? 1
remove_const<double> == double? 1
=== Standard <type_traits> ===
is_pointer<int>: 0
is_pointer<int*>: 1
is_const<const int>: 1
is_same<int, int>: 1
is_same<int, double>: 0
=== C++17 _v shortcuts ===
is_pointer_v<int*>: 1
is_const_v<const float>: 1
is_same_v<int, int>: 1
=== C++14 _t shortcuts ===
remove_cv_t<const volatile int> == int? 1
remove_reference_t<int&> == int? 1
Step-by-step explanation:
- The primary template defines the default answer — for
my_is_pointer, the default isfalse. Template specializations for specific patterns (likeT*) override the default for those patterns. The compiler selects the most specific matching specialization. - Inheriting from
false_type(which isintegral_constant<bool, false>) is the idiomatic way to define a false-valued trait.true_typeisintegral_constant<bool, true>. Both provide::value,::type, andoperator bool(). my_remove_const<const T>::typeisT— the specialization stripsconst.my_remove_const<T>::typefor non-constTis justTunchanged — the primary template returnsTas-is.- The
_vsuffix (C++17) is a variable template shortcut:is_pointer_v<T>is equivalent tois_pointer<T>::value. The_tsuffix (C++14) is a type alias shortcut:remove_const_t<T>is equivalent totypename remove_const<T>::type. Always prefer_vand_tin modern code — they are cleaner and less error-prone. is_same<int, int>::valueistrueonly when both template arguments are exactly the same type.is_same<int, const int>::valueisfalse—constis part of the type.
Query Traits: Asking Questions About Types
The standard library provides a rich set of query traits organized into categories:
#include <iostream>
#include <type_traits>
#include <string>
#include <vector>
using namespace std;
struct EmptyClass {};
struct NonTrivial {
NonTrivial() { /* user-defined */ }
virtual ~NonTrivial() {}
string name;
};
struct Pod { int x; double y; }; // Plain old data
void demonstrateQueryTraits() {
cout << "=== Primary type categories ===" << endl;
// Exactly one of these is true for any type
cout << "is_void<void>: " << is_void_v<void> << endl;
cout << "is_integral<int>: " << is_integral_v<int> << endl;
cout << "is_integral<char>: " << is_integral_v<char> << endl;
cout << "is_integral<bool>: " << is_integral_v<bool> << endl;
cout << "is_floating_point<double>: " << is_floating_point_v<double> << endl;
cout << "is_array<int[5]>: " << is_array_v<int[5]> << endl;
cout << "is_pointer<int*>: " << is_pointer_v<int*> << endl;
cout << "is_reference<int&>: " << is_reference_v<int&> << endl;
cout << "is_class<string>: " << is_class_v<string> << endl;
cout << "is_enum<enum E{}>: ";
enum Color { Red, Green, Blue };
cout << is_enum_v<Color> << endl;
cout << "is_function<int(int)>: " << is_function_v<int(int)> << endl;
cout << "\n=== Composite categories ===" << endl;
cout << "is_arithmetic<int>: " << is_arithmetic_v<int> << endl;
cout << "is_arithmetic<double>: " << is_arithmetic_v<double> << endl;
cout << "is_arithmetic<string>: " << is_arithmetic_v<string> << endl;
cout << "is_fundamental<int>: " << is_fundamental_v<int> << endl;
cout << "is_fundamental<string>: " << is_fundamental_v<string> << endl;
cout << "is_object<int>: " << is_object_v<int> << endl;
cout << "is_scalar<int*>: " << is_scalar_v<int*> << endl;
cout << "is_compound<string>: " << is_compound_v<string> << endl;
cout << "\n=== Type properties ===" << endl;
cout << "is_const<const int>: " << is_const_v<const int> << endl;
cout << "is_volatile<volatile int>: " << is_volatile_v<volatile int> << endl;
cout << "is_trivial<Pod>: " << is_trivial_v<Pod> << endl;
cout << "is_trivial<NonTrivial>: " << is_trivial_v<NonTrivial> << endl;
cout << "is_standard_layout<Pod>: " << is_standard_layout_v<Pod> << endl;
cout << "is_empty<EmptyClass>: " << is_empty_v<EmptyClass> << endl;
cout << "is_polymorphic<NonTrivial>: " << is_polymorphic_v<NonTrivial> << endl;
cout << "is_abstract<NonTrivial>: " << is_abstract_v<NonTrivial> << endl;
cout << "has_virtual_destructor<NonTrivial>: "
<< has_virtual_destructor_v<NonTrivial> << endl;
cout << "\n=== Constructor/destructor properties ===" << endl;
cout << "is_default_constructible<Pod>: "
<< is_default_constructible_v<Pod> << endl;
cout << "is_copy_constructible<string>: "
<< is_copy_constructible_v<string> << endl;
cout << "is_move_constructible<string>: "
<< is_move_constructible_v<string> << endl;
cout << "is_copy_assignable<string>: "
<< is_copy_assignable_v<string> << endl;
cout << "is_trivially_copyable<Pod>: "
<< is_trivially_copyable_v<Pod> << endl;
cout << "is_trivially_copyable<NonTrivial>: "
<< is_trivially_copyable_v<NonTrivial> << endl;
cout << "is_nothrow_move_constructible<Pod>: "
<< is_nothrow_move_constructible_v<Pod> << endl;
cout << "\n=== Size and alignment ===" << endl;
cout << "alignment_of<double>: " << alignment_of_v<double> << endl;
cout << "alignment_of<int>: " << alignment_of_v<int> << endl;
cout << "rank<int[2][3][4]>: " << rank_v<int[2][3][4]> << endl;
cout << "extent<int[2][3], 0>: " << extent_v<int[2][3], 0> << endl;
cout << "extent<int[2][3], 1>: " << extent_v<int[2][3], 1> << endl;
}
int main() {
demonstrateQueryTraits();
return 0;
}
Output:
=== Primary type categories ===
is_void<void>: 1
is_integral<int>: 1
is_integral<char>: 1
is_integral<bool>: 1
is_floating_point<double>: 1
is_array<int[5]>: 1
is_pointer<int*>: 1
is_reference<int&>: 1
is_class<string>: 1
is_enum<enum E{}>: 1
is_function<int(int)>: 1
=== Composite categories ===
is_arithmetic<int>: 1
is_arithmetic<double>: 1
is_arithmetic<string>: 0
is_fundamental<int>: 1
is_fundamental<string>: 0
is_object<int>: 1
is_scalar<int*>: 1
is_compound<string>: 1
=== Type properties ===
is_const<const int>: 1
is_volatile<volatile int>: 1
is_trivial<Pod>: 1
is_trivial<NonTrivial>: 0
is_standard_layout<Pod>: 1
is_empty<EmptyClass>: 1
is_polymorphic<NonTrivial>: 1
is_abstract<NonTrivial>: 0
has_virtual_destructor<NonTrivial>: 1
=== Constructor/destructor properties ===
is_default_constructible<Pod>: 1
is_copy_constructible<string>: 1
is_move_constructible<string>: 1
is_copy_assignable<string>: 1
is_trivially_copyable<Pod>: 1
is_trivially_copyable<NonTrivial>: 0
is_nothrow_move_constructible<Pod>: 1
=== Size and alignment ===
alignment_of<double>: 8
alignment_of<int>: 4
rank<int[2][3][4]>: 3
extent<int[2][3], 0>: 2
extent<int[2][3], 1>: 3
Step-by-step explanation:
- Primary type categories are mutually exclusive — every type belongs to exactly one.
boolis bothis_integralandis_arithmeticbecauseboolis an integral type in C++. is_trivially_copyable<T>is the trait you check before usingmemcpyto copy objects. Iftrue, the type’s bytes can be copied withmemcpy.Podqualifies;NonTrivial(with a user-defined constructor andstringmember) does not.is_polymorphic<T>istrueifThas at least one virtual function (directly or inherited) — meaning it has a vtable. This detects whether an object carries the vtable pointer overhead.rank<T>returns the number of array dimensions.extent<T, N>returns the size of dimensionN. These let you reason about array types generically.is_nothrow_move_constructible<T>is the trait thatstd::vectorchecks when deciding whether to move or copy elements during reallocation. If move construction isnoexcept, it moves (fast, O(1)); otherwise it copies (safe, but potentially O(n)) to preserve the strong exception guarantee.
Transformation Traits: Modifying Types
Transformation traits produce new types from existing ones. They are the building blocks of generic type manipulation.
#include <iostream>
#include <type_traits>
using namespace std;
int main() {
cout << "=== cv-qualifier transformations ===" << endl;
// Remove const, volatile, or both (cv)
static_assert(is_same_v<remove_const_t<const int>, int>);
static_assert(is_same_v<remove_volatile_t<volatile int>, int>);
static_assert(is_same_v<remove_cv_t<const volatile int>, int>);
// Add const/volatile
static_assert(is_same_v<add_const_t<int>, const int>);
static_assert(is_same_v<add_volatile_t<int>, volatile int>);
static_assert(is_same_v<add_cv_t<int>, const volatile int>);
cout << "=== Reference transformations ===" << endl;
static_assert(is_same_v<remove_reference_t<int&>, int>);
static_assert(is_same_v<remove_reference_t<int&&>, int>);
static_assert(is_same_v<remove_reference_t<int>, int>); // unchanged
static_assert(is_same_v<add_lvalue_reference_t<int>, int&>);
static_assert(is_same_v<add_rvalue_reference_t<int>, int&&>);
cout << "=== Pointer transformations ===" << endl;
static_assert(is_same_v<remove_pointer_t<int*>, int>);
static_assert(is_same_v<remove_pointer_t<int**>, int*>); // Only one level
static_assert(is_same_v<add_pointer_t<int>, int*>);
cout << "=== decay: the most important transformation ===" << endl;
// decay<T> models what happens when T is passed by value to a function:
// - arrays decay to pointers
// - functions decay to function pointers
// - cv-qualifiers are stripped
static_assert(is_same_v<decay_t<int[5]>, int*>); // array → pointer
static_assert(is_same_v<decay_t<int(double)>, int(*)(double)>); // function → ptr
static_assert(is_same_v<decay_t<const int&>, int>); // ref + const stripped
static_assert(is_same_v<decay_t<int&&>, int>); // rvalue ref stripped
static_assert(is_same_v<decay_t<int>, int>); // unchanged
cout << "=== Conditional type selection ===" << endl;
// conditional<bool, T, F>::type: if bool is true, T; else F
using BigOrSmall = conditional_t<(sizeof(int) > 2), long long, short>;
static_assert(is_same_v<BigOrSmall, long long>); // On 32/64-bit systems
cout << "BigOrSmall is 64-bit: " << (sizeof(BigOrSmall) == 8 ? "yes" : "no") << endl;
// Nested conditional for type selection based on multiple conditions
template_arg_demo();
cout << "=== make_signed / make_unsigned ===" << endl;
static_assert(is_same_v<make_signed_t<unsigned int>, int>);
static_assert(is_same_v<make_unsigned_t<int>, unsigned int>);
static_assert(is_same_v<make_signed_t<unsigned char>, signed char>);
cout << "=== common_type ===" << endl;
// common_type: the type that all given types can be converted to
using CT1 = common_type_t<int, double>; // double
using CT2 = common_type_t<int, long, unsigned>; // unsigned long
cout << "common_type<int, double>: "
<< (is_same_v<CT1, double> ? "double" : "other") << endl;
cout << "All static_asserts passed — transformations verified!" << endl;
return 0;
}
void template_arg_demo() {
// Select storage type based on size
template_select<4>();
template_select<8>();
template_select<16>();
}
template<size_t N>
void template_select() {
using StorageType =
conditional_t<(N <= 4), int,
conditional_t<(N <= 8), long long,
__int128>>;
cout << "Size " << N << " → storage is "
<< sizeof(StorageType) << " bytes" << endl;
}
Output:
=== cv-qualifier transformations ===
=== Reference transformations ===
=== Pointer transformations ===
=== decay: the most important transformation ===
=== Conditional type selection ===
BigOrSmall is 64-bit: yes
Size 4 → storage is 4 bytes
Size 8 → storage is 8 bytes
Size 16 → storage is 16 bytes
=== make_signed / make_unsigned ===
=== common_type ===
common_type<int, double>: double
All static_asserts passed — transformations verified!
Step-by-step explanation:
decay_t<T>is the single most used transformation trait. It models “what type doesTbecome when stored as a value?” — stripping references, top-level cv-qualifiers, decaying arrays to pointers and functions to function pointers.autovariable deduction andstd::make_pairusedecay_tinternally.conditional_t<B, T, F>is the compile-time ternary operator: ifBistrue, the result isT; otherwiseF. Nestingconditional_timplements a compile-timeif/else if/elsechain for type selection.common_type_t<T1, T2, ...>finds the type to which all given types can be implicitly converted. Used in generic arithmetic functions:template<typename T, typename U> common_type_t<T,U> add(T a, U b) { return a + b; }— the return type isdoubleif you passintanddouble.make_signed_t/make_unsigned_tconvert between signed and unsigned variants of the same integer type. Useful when you have a generic integer type and need to ensure correct signedness for arithmetic or comparison operations.static_assert(expr)with type traits evaluates the assertion at compile time. If the assertion fails, you get a compile error with the message — zero runtime cost. This is how you write compile-time unit tests for type manipulations.
Relationship Traits: Comparing Types
#include <iostream>
#include <type_traits>
using namespace std;
struct Base { virtual ~Base() = default; };
struct Derived : Base {};
struct Unrelated {};
class NonCopyable {
NonCopyable(const NonCopyable&) = delete;
public:
NonCopyable() = default;
};
int main() {
cout << "=== is_same ===" << endl;
cout << "is_same<int, int>: " << is_same_v<int, int> << endl;
cout << "is_same<int, const int>: " << is_same_v<int, const int> << endl; // false!
cout << "is_same<int, signed int>: " << is_same_v<int, signed int> << endl; // true
cout << "\n=== is_base_of ===" << endl;
cout << "is_base_of<Base, Derived>: " << is_base_of_v<Base, Derived> << endl;
cout << "is_base_of<Derived, Base>: " << is_base_of_v<Derived, Base> << endl;
cout << "is_base_of<Base, Base>: " << is_base_of_v<Base, Base> << endl;
cout << "is_base_of<Base, Unrelated>: " << is_base_of_v<Base, Unrelated> << endl;
cout << "\n=== is_convertible ===" << endl;
cout << "is_convertible<int, double>: " << is_convertible_v<int, double> << endl;
cout << "is_convertible<double, int>: " << is_convertible_v<double, int> << endl;
cout << "is_convertible<Derived*, Base*>:" << is_convertible_v<Derived*, Base*><< endl;
cout << "is_convertible<Base*, Derived*>:" << is_convertible_v<Base*, Derived*><< endl;
cout << "is_convertible<int, string>: " << is_convertible_v<int, string> << endl;
cout << "\n=== is_assignable ===" << endl;
// is_assignable<T, U>: can you do "T t; t = U{}"?
cout << "is_assignable<int&, int>: " << is_assignable_v<int&, int> << endl;
cout << "is_assignable<int&, double>: " << is_assignable_v<int&, double> << endl;
cout << "is_assignable<int, int>: " << is_assignable_v<int, int> << endl;
cout << "is_assignable<NonCopyable&, NonCopyable>: "
<< is_assignable_v<NonCopyable&, NonCopyable> << endl;
cout << "\n=== is_invocable (C++17) ===" << endl;
// is_invocable<F, Args...>: can you call F with Args?
auto lambda = [](int x, double y) { return x + y; };
cout << "lambda invocable(int, double): "
<< is_invocable_v<decltype(lambda), int, double> << endl;
cout << "lambda invocable(string): "
<< is_invocable_v<decltype(lambda), string> << endl;
cout << "invoke_result<lambda, int, double> is double? "
<< is_same_v<invoke_result_t<decltype(lambda), int, double>, double> << endl;
return 0;
}
Output:
=== is_same ===
is_same<int, int>: 1
is_same<int, const int>: 0
is_same<int, signed int>: 1
=== is_base_of ===
is_base_of<Base, Derived>: 1
is_base_of<Derived, Base>: 0
is_base_of<Base, Base>: 1
is_base_of<Base, Unrelated>: 0
=== is_convertible ===
is_convertible<int, double>: 1
is_convertible<double, int>: 1
is_convertible<Derived*, Base*>:1
is_convertible<Base*, Derived*>:0
is_convertible<int, string>: 0
=== is_assignable ===
is_assignable<int&, int>: 1
is_assignable<int&, double>: 1
is_assignable<int, int>: 0
is_assignable<NonCopyable&, NonCopyable>: 0
=== is_invocable (C++17) ===
lambda invocable(int, double): 1
lambda invocable(string): 0
invoke_result<lambda, int, double> is double? 1
Step-by-step explanation:
is_same<int, const int>isfalse—constis part of the type. This surprises many beginners who expectconst intto “be” anint. The types are distinct;remove_const_t<const int>producesint.is_base_of<B, D>istrueeven whenB == D— a class is considered its own base. It is alsotruefor private base classes. Use it to check inheritance relationships in templates.is_convertible<From, To>checks whether an implicit conversion fromFromtoToexists.doubletointis convertible (narrowing, but implicit).Base*toDerived*is not implicitly convertible (requires explicit cast).inttostringis not implicitly convertible at all.is_assignable<T, U>requiresTto be an lvalue reference —is_assignable<int, int>isfalsebecause you cannot assign to an rvalueint.is_assignable<int&, double>istruebecauseint x; x = 1.5;compiles (narrowing conversion).invoke_result_t<F, Args...>gives the return type of callingFwithArgs— the type-safe version ofdecltype(f(args...)).is_invocable_vchecks if the call is valid without actually performing it.
Writing Custom Type Traits
The real power emerges when you write your own type traits for domain-specific questions.
#include <iostream>
#include <type_traits>
#include <vector>
#include <string>
using namespace std;
// Trait 1: Detect if a type has a specific method
// Using void_t idiom (C++17)
// Primary template: no serialize() method
template<typename T, typename = void>
struct has_serialize : false_type {};
// Specialization: T has serialize() returning string
template<typename T>
struct has_serialize<T, void_t<
decltype(declval<T>().serialize())
>> : is_same<decltype(declval<T>().serialize()), string> {};
template<typename T>
inline constexpr bool has_serialize_v = has_serialize<T>::value;
// Trait 2: Detect if T is a container (has begin/end/size)
template<typename T, typename = void>
struct is_container : false_type {};
template<typename T>
struct is_container<T, void_t<
decltype(declval<T>().begin()),
decltype(declval<T>().end()),
decltype(declval<T>().size())
>> : true_type {};
template<typename T>
inline constexpr bool is_container_v = is_container<T>::value;
// Trait 3: Detect if T supports operator<< with ostream
template<typename T, typename = void>
struct is_streamable : false_type {};
template<typename T>
struct is_streamable<T, void_t<
decltype(declval<ostream&>() << declval<T>())
>> : true_type {};
template<typename T>
inline constexpr bool is_streamable_v = is_streamable<T>::value;
// Trait 4: Get the element type of a container
template<typename T>
struct element_type { using type = T; }; // Non-container: element is T itself
template<typename T>
struct element_type<vector<T>> { using type = T; };
template<typename T, size_t N>
struct element_type<T[N]> { using type = T; };
template<typename T>
using element_type_t = typename element_type<T>::type;
// Trait 5: A numeric type trait for domain-specific logic
template<typename T>
struct is_numeric : integral_constant<bool,
is_arithmetic_v<T> && !is_same_v<T, bool> && !is_same_v<T, char>
> {};
template<typename T>
inline constexpr bool is_numeric_v = is_numeric<T>::value;
// --- Test classes ---
struct Serializable {
int id;
string name;
string serialize() const {
return "id=" + to_string(id) + ";name=" + name;
}
};
struct NotSerializable {
int x;
};
struct NotStreamable {
double privateData;
// No operator<< defined
};
// --- Generic functions using custom traits ---
template<typename T>
void smartPrint(const T& value) {
if constexpr (is_streamable_v<T>) {
cout << " streamable: " << value << endl;
} else if constexpr (has_serialize_v<T>) {
cout << " serializable: " << value.serialize() << endl;
} else {
cout << " opaque type, size=" << sizeof(T) << " bytes" << endl;
}
}
template<typename T>
void processContainer(const T& container) {
static_assert(is_container_v<T>, "T must be a container");
cout << "Container with " << container.size() << " elements, "
<< "element type size=" << sizeof(element_type_t<T>) << " bytes" << endl;
}
int main() {
cout << "=== Custom trait detection ===" << endl;
// has_serialize
cout << "has_serialize<Serializable>: " << has_serialize_v<Serializable> << endl;
cout << "has_serialize<NotSerializable>: " << has_serialize_v<NotSerializable> << endl;
cout << "has_serialize<string>: " << has_serialize_v<string> << endl;
// is_container
cout << "is_container<vector<int>>: " << is_container_v<vector<int>> << endl;
cout << "is_container<string>: " << is_container_v<string> << endl;
cout << "is_container<int>: " << is_container_v<int> << endl;
// is_streamable
cout << "is_streamable<int>: " << is_streamable_v<int> << endl;
cout << "is_streamable<string>: " << is_streamable_v<string> << endl;
cout << "is_streamable<NotStreamable>:" << is_streamable_v<NotStreamable><< endl;
// is_numeric
cout << "is_numeric<int>: " << is_numeric_v<int> << endl;
cout << "is_numeric<double>: " << is_numeric_v<double> << endl;
cout << "is_numeric<bool>: " << is_numeric_v<bool> << endl; // false
cout << "is_numeric<char>: " << is_numeric_v<char> << endl; // false
cout << "\n=== smartPrint with type branching ===" << endl;
smartPrint(42); // streamable: int
smartPrint(string("hello")); // streamable: string
smartPrint(Serializable{1, "Alice"}); // serializable: no operator<<
smartPrint(NotStreamable{3.14}); // opaque
cout << "\n=== processContainer ===" << endl;
vector<double> dv = {1.1, 2.2, 3.3};
string s = "hello";
processContainer(dv);
processContainer(s);
cout << "\n=== element_type ===" << endl;
cout << "element_type_t<vector<int>>: int? "
<< is_same_v<element_type_t<vector<int>>, int> << endl;
cout << "element_type_t<double[10]>: double? "
<< is_same_v<element_type_t<double[10]>, double> << endl;
return 0;
}
Output:
=== Custom trait detection ===
has_serialize<Serializable>: 1
has_serialize<NotSerializable>: 0
has_serialize<string>: 0
is_container<vector<int>>: 1
is_container<string>: 1
is_container<int>: 0
is_streamable<int>: 1
is_streamable<string>: 1
is_streamable<NotStreamable>:0
is_numeric<int>: 1
is_numeric<double>: 1
is_numeric<bool>: 0
is_numeric<char>: 0
=== smartPrint with type branching ===
streamable: 42
streamable: hello
serializable: id=1;name=Alice
opaque type, size=8 bytes
=== processContainer ===
Container with 3 elements, element type size=8 bytes
Container with 5 elements, element type size=1 bytes
=== element_type ===
element_type_t<vector<int>>: int? 1
element_type_t<double[10]>: double? 1
Step-by-step explanation:
void_tidiom is the key technique for detecting methods.void_t<expr>isvoidifexpris valid, and causes substitution failure (SFINAE) ifexpris invalid. The specializationstruct has_serialize<T, void_t<decltype(declval<T>().serialize())>>only exists whenT::serialize()is valid.declval<T>()produces an rvalue of typeTwithout constructing one — usable in unevaluated contexts likedecltype. This is how you “call” a method on a type without an actual object.smartPrintusesif constexprto branch on trait values. Each branch is compiled only if its condition is true — so the branch forserializable(which calls.serialize()) is not compiled for types likeintthat don’t have that method.is_numeric<T>shows trait composition: build a new trait from existing ones usingintegral_constant. This excludesboolandcharfrom “numeric” even though they are technically integral — a domain-specific decision appropriate for many mathematical APIs.- The
void_tapproach is C++17 standard. In C++20, Concepts provide a cleaner syntax for the same detection, butvoid_ttraits remain useful for backward compatibility and library code.
Type Traits with if constexpr: Compile-Time Branching
The combination of type traits and if constexpr (C++17) is one of the most useful patterns in modern generic programming:
#include <iostream>
#include <type_traits>
#include <string>
#include <vector>
using namespace std;
// Generic to_string that handles any type sensibly
template<typename T>
string universalToString(const T& value) {
if constexpr (is_same_v<T, string>) {
return value; // Already a string
} else if constexpr (is_arithmetic_v<T>) {
return std::to_string(value); // Numbers: use std::to_string
} else if constexpr (is_pointer_v<T>) {
if (value == nullptr) return "nullptr";
ostringstream oss;
oss << "0x" << hex << reinterpret_cast<uintptr_t>(value);
return oss.str(); // Pointers: hex address
} else if constexpr (is_array_v<T>) {
string result = "[";
for (size_t i = 0; i < extent_v<T>; i++) {
if (i > 0) result += ", ";
result += universalToString(value[i]);
}
return result + "]";
} else {
return "{object of size " + std::to_string(sizeof(T)) + "}";
}
}
// Generic copy: uses memcpy for trivially copyable types (fast path)
template<typename T>
void genericCopy(T* dest, const T* src, size_t count) {
if constexpr (is_trivially_copyable_v<T>) {
// Fast path: trivially copyable, use memcpy
memcpy(dest, src, count * sizeof(T));
cout << " Using memcpy for trivially copyable type" << endl;
} else {
// Slow path: call copy constructor for each element
for (size_t i = 0; i < count; i++) {
new (dest + i) T(src[i]);
}
cout << " Using copy constructor for non-trivial type" << endl;
}
}
// Generic absolute value that handles signed/unsigned correctly
template<typename T>
T genericAbs(T value) {
if constexpr (is_unsigned_v<T>) {
return value; // Unsigned: always non-negative
} else if constexpr (is_floating_point_v<T>) {
return value < 0 ? -value : value;
} else {
// Signed integer: handle INT_MIN carefully
return value < 0 ? -value : value;
}
}
struct ComplexObject {
string name;
vector<int> data;
ComplexObject(string n, vector<int> d) : name(n), data(d) {}
ComplexObject(const ComplexObject&) = default;
};
int main() {
cout << "=== universalToString ===" << endl;
cout << universalToString(42) << endl;
cout << universalToString(3.14) << endl;
cout << universalToString(string("hi"))<< endl;
int x = 5;
cout << universalToString(&x) << endl;
cout << universalToString((int*)nullptr)<< endl;
int arr[] = {1, 2, 3, 4, 5};
cout << universalToString(arr) << endl;
cout << universalToString(ComplexObject{"test", {}})<< endl;
cout << "\n=== genericCopy ===" << endl;
// Trivially copyable: uses memcpy
int src[5] = {1, 2, 3, 4, 5};
int dst[5];
genericCopy(dst, src, 5);
cout << " Copied: ";
for (int v : dst) cout << v << " ";
cout << endl;
// Non-trivially copyable: uses copy constructor
ComplexObject srcObj("Alice", {10, 20, 30});
alignas(ComplexObject) char dstBuf[sizeof(ComplexObject)];
ComplexObject* dstObj = reinterpret_cast<ComplexObject*>(dstBuf);
genericCopy(dstObj, &srcObj, 1);
cout << " Copied name: " << dstObj->name << endl;
dstObj->~ComplexObject();
cout << "\n=== genericAbs ===" << endl;
cout << "abs(-5): " << genericAbs(-5) << endl;
cout << "abs(-3.14): " << genericAbs(-3.14) << endl;
cout << "abs(5u): " << genericAbs(5u) << endl;
return 0;
}
Output:
=== universalToString ===
42
3.140000
hi
0x7ffd...
nullptr
[1, 2, 3, 4, 5]
{object of size 56}
=== genericCopy ===
Using memcpy for trivially copyable type
Copied: 1 2 3 4 5
Using copy constructor for non-trivial type
Copied name: Alice
=== genericAbs ===
abs(-5): 5
abs(-3.14): 3.14
abs(5u): 5
Step-by-step explanation:
- Each
if constexprbranch is only instantiated if its condition is true. ForuniversalToString(42), theis_arithmetic_v<T>branch is the one compiled — theis_array_v<T>branch, which accessesextent_v<T>, is not compiled (and would fail forint). genericCopydemonstrates the classic optimization:is_trivially_copyable_v<T>selectsmemcpy(one fast hardware instruction) for plain data types, and the copy constructor loop for complex types. This is exactly howstd::copyis optimized in standard library implementations.- The
if constexprbranches can access type-specific operations without breaking compilation for other types.extent_v<T>(the array size) is only computed in theis_array_v<T>branch, where it is valid. genericAbswithif constexpravoids applying the negation-valueto unsigned types, which would produce unexpected large values. The unsigned branchreturn valueis zero-cost — compiles to nothing.
Type Traits Quick Reference
| Category | Trait | What it checks / produces |
|---|---|---|
| Primary | is_integral_v<T> |
int, char, bool, etc. |
| Primary | is_floating_point_v<T> |
float, double, long double |
| Primary | is_pointer_v<T> |
T* or const T* |
| Primary | is_array_v<T> |
T[N] or T[] |
| Primary | is_class_v<T> |
struct or class type |
| Composite | is_arithmetic_v<T> |
integral or floating point |
| Composite | is_scalar_v<T> |
arithmetic, pointer, enum, nullptr_t |
| Property | is_const_v<T> |
top-level const qualifier |
| Property | is_trivially_copyable_v<T> |
safe to memcpy |
| Property | is_polymorphic_v<T> |
has virtual functions |
| Property | is_empty_v<T> |
no non-static data members |
| Operation | is_constructible_v<T, Args...> |
T(args) is valid |
| Operation | is_copy_constructible_v<T> |
T(const T&) is valid |
| Operation | is_nothrow_move_constructible_v<T> |
noexcept move ctor |
| Operation | is_invocable_v<F, Args...> |
f(args) is valid |
| Relation | is_same_v<T, U> |
T and U are identical |
| Relation | is_base_of_v<B, D> |
B is base class of D |
| Relation | is_convertible_v<From, To> |
implicit conversion exists |
| Transform | remove_const_t<T> |
strips top-level const |
| Transform | remove_reference_t<T> |
strips & or && |
| Transform | decay_t<T> |
models pass-by-value |
| Transform | conditional_t<B, T, F> |
B ? T : F at compile time |
| Transform | common_type_t<T, U> |
common convertible type |
| Size | alignment_of_v<T> |
alignment requirement |
| Size | rank_v<T> |
number of array dimensions |
Conclusion
Type traits are the compile-time reflection system of C++. They answer questions about types, transform types into related types, and compare types — all at compile time, with zero runtime cost. Understanding them unlocks the full power of generic programming: instead of writing separate functions for int vs double vs string, you write one function that uses type traits to select the correct behavior for each type automatically.
The standard library’s <type_traits> provides over 100 traits covering every aspect of the C++ type system: primary categories, composite categories, cv-qualifiers, references, pointers, special member functions, constructibility, and relationships between types. The C++17 _v variable templates and C++14 _t type aliases make these traits readable and ergonomic.
Custom type traits — built with the void_t idiom and template specialization — let you define your own compile-time predicates: does this type have a .serialize() method? Is it a container? Does it support streaming? These power the if constexpr branches and SFINAE constraints that make generic libraries both flexible and safe.
Together with if constexpr, SFINAE, and C++20 Concepts, type traits form the backbone of modern C++ template programming — enabling code that adapts to type properties at compile time, generating optimal machine code for each type without any runtime dispatch overhead.




