Type Traits and Metaprogramming — Compile-Time Type Reflection, std::is_same, std::conditional, std::invoke_result
In this tutorial, you will learn about Type Traits and Metaprogramming. We cover key concepts, practical examples, and best practices to help you master this topic.
C++ type traits are compile-time type predicates and transformations — such as std::is_integral, std::conditional, and std::invoke_result — that enable metaprogramming by inspecting and manipulating types without runtime overhead.
What You'll Learn
You will use standard type traits to query type properties and relationships, apply type transformations like add_pointer, remove_reference, and conditional, combine traits with if constexpr for conditional compilation, write custom type traits using template specialization and SFINAE, and understand how traits power std::invoke, std::visit, and utility libraries.
Why It Matters
Type traits are the foundation of compile-time Reflection in C++. Every time you use std::is_integral_v<T>, std::is_same_v<T, U>, or std::invoke_result_t<F, Args...>, you use type traits. They enable generic algorithms that adapt to type properties — for example, using memcpy for trivially copyable types and copy constructors otherwise. C++ libraries from the STL to Boost are built on type traits.
Learning Path
graph LR
A["48: Concepts & Requires"] --> B["49: Type Traits & Metaprogramming"]
B --> C["50: Lambda Expressions"]
C --> D["51: auto & decltype"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
Primary Type Categories
Primary type categories answer "what kind of type is this?"
#include <iostream>
#include <type_traits>
template <typename T>
void describeType() {
std::cout << "void: " << std::is_void_v<T> << "\n";
std::cout << "integral: " << std::is_integral_v<T> << "\n";
std::cout << "floating_point: " << std::is_floating_point_v<T> << "\n";
std::cout << "pointer: " << std::is_pointer_v<T> << "\n";
std::cout << "reference: " << std::is_reference_v<T> << "\n";
std::cout << "array: " << std::is_array_v<T> << "\n";
std::cout << "class: " << std::is_class_v<T> << "\n";
std::cout << "enum: " << std::is_enum_v<T> << "\n";
std::cout << "union: " << std::is_union_v<T> << "\n";
std::cout << "function: " << std::is_function_v<T> << "\n";
}
int main() {
describeType<int>();
// void: 0, integral: 1, floating_point: 0, pointer: 0, ...
describeType<double*>();
// pointer: 1
describeType<std::string>();
// class: 1
}
Type Relationships
Query relationships between types.
#include <iostream>
#include <type_traits>
struct Base {};
struct Derived : Base {};
int main() {
std::cout << std::boolalpha;
// Same type
std::cout << "is_same<int, int>: " << std::is_same_v<int, int> << "\n"; // true
std::cout << "is_same<int, long>: " << std::is_same_v<int, long> << "\n"; // false
// Convertibility
std::cout << "convertible<int, double>: "
<< std::is_convertible_v<int, double> << "\n"; // true
// Inheritance
std::cout << "base_of<Base, Derived>: "
<< std::is_base_of_v<Base, Derived> << "\n"; // true
// Assignment
std::cout << "assignable<int&, int>: "
<< std::is_assignable_v<int&, int> << "\n"; // true
// Constructible
std::cout << "constructible<string, const char*>: "
<< std::is_constructible_v<std::string, const char*> << "\n"; // true
std::cout << "default_constructible<int>: "
<< std::is_default_constructible_v<int> << "\n"; // true
// Trivially copyable (memcpy-safe)
std::cout << "trivially_copyable<int>: "
<< std::is_trivially_copyable_v<int> << "\n"; // true
std::cout << "trivially_copyable<string>: "
<< std::is_trivially_copyable_v<std::string> << "\n"; // false
}
Type Transformations
Transformations produce a new type from an input type.
#include <iostream>
#include <type_traits>
int main() {
// Add/remove qualifiers
using RawInt = int;
using ConstInt = std::add_const_t<RawInt>; // const int
using UnconstInt = std::remove_const_t<ConstInt>; // int
using IntRef = std::add_lvalue_reference_t<RawInt>; // int&
using IntPtr = std::add_pointer_t<RawInt>; // int*
// Remove reference
using RefToInt = int&;
using UnrefInt = std::remove_reference_t<RefToInt>; // int
// Decay (remove reference + cv-qualifiers, array->ptr, function->ptr)
using Decayed = std::decay_t<const int&>; // int
// Conditional: select type based on compile-time condition
using Selected = std::conditional_t<true, int, double>; // int
using Selected2 = std::conditional_t<false, int, double>; // double
// Void_t: map anything to void (SFINAE helper)
using VoidFromInt = std::void_t<int>; // void
// Common type (find type both can convert to)
using Common = std::common_type_t<int, double>; // double
std::cout << "Common type of int and double: "
<< std::is_same_v<Common, double> << "\n"; // true
}
Utility Type Traits
Helper traits for detecting function signatures and member types.
#include <iostream>
#include <type_traits>
#include <vector>
struct MyClass {
using value_type = int;
void method(int) {}
};
int main() {
// Member types
using VT = typename MyClass::value_type;
std::cout << "value_type is int: " << std::is_same_v<VT, int> << "\n"; // true
// Detecting member types with void_t
template <typename, typename = void>
struct HasValueType : std::false_type {};
template <typename T>
struct HasValueType<T, std::void_t<typename T::value_type>>
: std::true_type {};
std::cout << "Has value_type: " << HasValueType<MyClass>::value << "\n"; // true
std::cout << "Has value_type (int): " << HasValueType<int>::value << "\n"; // false
// Invoke result (C++17)
auto lambda = [](int x, double y) { return x + y; };
using Result = std::invoke_result_t<decltype(lambda), int, double>;
std::cout << "Invoke result is double: "
<< std::is_same_v<Result, double> << "\n"; // true
}
Custom Type Traits
You can build your own traits using template specialization and SFINAE.
#include <iostream>
#include <type_traits>
#include <vector>
#include <list>
// Custom trait: is_container (has begin/end + value_type)
template <typename, typename = void>
struct IsContainer : std::false_type {};
template <typename T>
struct IsContainer<T, std::void_t<
typename T::value_type,
decltype(std::declval<T>().begin()),
decltype(std::declval<T>().end())
>> : std::true_type {};
template <typename T>
constexpr bool IsContainer_v = IsContainer<T>::value;
// Custom trait: is_reservable
template <typename, typename = void>
struct IsReservable : std::false_type {};
template <typename T>
struct IsReservable<T, std::void_t<
decltype(std::declval<T>().reserve(0))
>> : std::true_type {};
template <typename T>
constexpr bool IsReservable_v = IsReservable<T>::value;
// Function using custom traits
template <typename Container>
void prepare(Container& c, size_t n) {
if constexpr (IsReservable_v<Container>) {
c.reserve(n);
std::cout << "Reserved " << n << "\n";
} else {
std::cout << "Cannot reserve, skipping\n";
}
}
int main() {
std::cout << "vector is container: " << IsContainer_v<std::vector<int>> << "\n"; // 1
std::cout << "list is container: " << IsContainer_v<std::list<int>> << "\n"; // 1
std::cout << "int is container: " << IsContainer_v<int> << "\n"; // 0
std::cout << "vector is reservable: " << IsReservable_v<std::vector<int>> << "\n"; // 1
std::cout << "list is reservable: " << IsReservable_v<std::list<int>> << "\n"; // 0
std::vector<int> v;
prepare(v, 100); // Reserved 100
std::list<int> lst;
prepare(lst, 100); // Cannot reserve, skipping
}
Conditional Overloading with Traits
Combine traits with if constexpr for type-optimized implementations.
#include <iostream>
#include <type_traits>
#include <cstring>
// Copy that uses memcpy for trivially copyable types
template <typename T>
T* fastCopy(const T* src, T* dst, size_t count) {
if constexpr (std::is_trivially_copyable_v<T>) {
std::memcpy(dst, src, count * sizeof(T));
std::cout << "Using memcpy\n";
} else {
for (size_t i = 0; i < count; ++i) {
dst[i] = src[i];
}
std::cout << "Using copy constructor\n";
}
return dst;
}
struct NonTrivial {
int data;
NonTrivial& operator=(const NonTrivial& other) {
data = other.data;
return *this;
}
};
int main() {
int int_src[] = {1, 2, 3, 4, 5};
int int_dst[5];
fastCopy(int_src, int_dst, 5); // Using memcpy
NonTrivial nt_src[3] = {{1}, {2}, {3}};
NonTrivial nt_dst[3];
fastCopy(nt_src, nt_dst, 3); // Using copy constructor
}
Type Lists and Compile-Time Algorithms
Advanced metaprogramming manipulates lists of types.
#include <iostream>
#include <type_traits>
// Type list
template <typename...>
struct TypeList {};
// Length of type list
template <typename>
struct TypeListLength;
template <typename... Types>
struct TypeListLength<TypeList<Types...>>
: std::integral_constant<size_t, sizeof...(Types)> {};
template <typename List>
constexpr size_t TypeListLength_v = TypeListLength<List>::value;
// Index access
template <size_t, typename>
struct TypeListGet;
template <typename Head, typename... Tail>
struct TypeListGet<0, TypeList<Head, Tail...>> {
using type = Head;
};
template <size_t Index, typename Head, typename... Tail>
struct TypeListGet<Index, TypeList<Head, Tail...>>
: TypeListGet<Index - 1, TypeList<Tail...>> {};
template <size_t Index, typename List>
using TypeListGet_t = typename TypeListGet<Index, List>::type;
// Find index of a type
template <typename, typename, size_t = 0>
struct TypeListFind;
template <typename T, typename... Rest, size_t Pos>
struct TypeListFind<T, TypeList<T, Rest...>, Pos>
: std::integral_constant<size_t, Pos> {};
template <typename T, typename Head, typename... Rest, size_t Pos>
struct TypeListFind<T, TypeList<Head, Rest...>, Pos>
: TypeListFind<T, TypeList<Rest...>, Pos + 1> {};
template <typename T, typename List>
constexpr size_t TypeListFind_v = TypeListFind<T, List>::value;
int main() {
using MyTypes = TypeList<int, double, char, float>;
std::cout << "Length: " << TypeListLength_v<MyTypes> << "\n"; // 4
std::cout << "Index 2 is char: "
<< std::is_same_v<TypeListGet_t<2, MyTypes>, char> << "\n"; // true
std::cout << "Index of double: "
<< TypeListFind_v<double, MyTypes> << "\n"; // 1
std::cout << "Index of float: "
<< TypeListFind_v<float, MyTypes> << "\n"; // 3
}
std::integral_constant and Value Wrappers
integral_constant wraps a compile-time value as a type.
#include <iostream>
#include <type_traits>
// std::true_type = integral_constant<bool, true>
// std::false_type = integral_constant<bool, false>
// std::integral_constant<T, v> wraps a value
template <bool B>
struct MyBool : std::integral_constant<bool, B> {};
// Using integral_constant for compile-time values
template <int N>
struct Factorial : std::integral_constant<int,
Factorial<N - 1>::value * N> {};
template <>
struct Factorial<0> : std::integral_constant<int, 1> {};
int main() {
std::cout << std::boolalpha;
std::cout << "true_type: " << std::true_type::value << "\n"; // true
std::cout << "false_type: " << std::false_type::value << "\n"; // false
std::cout << "5! = " << Factorial<5>::value << "\n"; // 120
std::cout << "10! = " << Factorial<10>::value << "\n"; // 3628800
}
Common Mistakes
Mistake 1: Using _v and _t suffixes without C++14/17
std::is_integral_v<int>; // C++17 (needs C++14 in some compilers)
std::is_integral<int>::value; // C++11 (always works)
Mistake 2: Type traits require complete types
struct Incomplete;
std::is_class_v<Incomplete>; // Undefined behavior
Forward-declared types are not complete. Check with std::is_complete_v first.
Mistake 3: Forgetting typename for dependent types
template <typename T>
using ValueType = T::value_type; // Error: need typename
template <typename T>
using ValueType = typename T::value_type; // OK
Mistake 4: Assuming is_same works with cv-qualified types
std::is_same_v<int, const int>; // false
std::is_same_v<int, std::remove_const_t<const int>>; // true
Mistake 5: Using type traits with incomplete or void types
Some traits like is_constructible work with void correctly, but is_class<void> is false.
Practice Questions
What does
std::conditional_t<true, int, double>produce? Answer:int— conditional selects the second type when the condition is true.What is
std::decay_t<const int&>? Answer:int— decay removes reference and top-level cv-qualifiers.How do you check if a type is trivially copyable? Answer:
std::is_trivially_copyable_v<T>— returns true if memcpy is safe.Write a trait
is_pair<T>that detectsstd::pair. Answer: Use template specialization:template <typename T, typename U> struct is_pair<std::pair<T, U>> : std::true_type {};.What does
std::void_t<int, double, char>produce? Answer:void— void_t maps any type sequence to void.
FAQ
Mini Project
Implement a generic TuplePrinter that prints all elements of a std::tuple using type traits and compile-time index sequences:
#include <iostream>
#include <tuple>
#include <string>
#include <type_traits>
// Your TuplePrinter here
int main() {
auto t1 = std::make_tuple(42, 3.14, "hello");
printTuple(t1); // (42, 3.14, hello)
auto t2 = std::make_tuple(1, "two", 3.0f, '4');
printTuple(t2); // (1, two, 3, 4)
// Edge case: single element
auto t3 = std::make_tuple("only");
printTuple(t3); // (only)
// Edge case: empty tuple
std::tuple<> t4;
printTuple(t4); // ()
}
This project ties together everything: variadic templates, index sequences, type traits, and SFINAE — the core toolkit of C++ metaprogramming. Compare with Java which lacks compile-time type computation entirely.
What's Next
You now understand type traits and template metaprogramming — the most powerful compile-time techniques in C++. Next, you will learn lambda expressions, one of the most practical features for writing concise, functional-style code in C++11 and beyond.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro