Skip to content

Type Traits and Metaprogramming — Compile-Time Type Reflection, std::is_same, std::conditional, std::invoke_result

DodaTech Updated 2026-06-28 10 min read

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

  1. What does std::conditional_t<true, int, double> produce? Answer: int — conditional selects the second type when the condition is true.

  2. What is std::decay_t<const int&>? Answer: int — decay removes reference and top-level cv-qualifiers.

  3. How do you check if a type is trivially copyable? Answer: std::is_trivially_copyable_v<T> — returns true if memcpy is safe.

  4. Write a trait is_pair<T> that detects std::pair. Answer: Use template specialization: template <typename T, typename U> struct is_pair<std::pair<T, U>> : std::true_type {};.

  5. What does std::void_t<int, double, char> produce? Answer: void — void_t maps any type sequence to void.

FAQ

What are C++ type traits

Type traits are compile-time templates that query or transform type properties. They include type predicates (is_integral), transformations (add_pointer), and queries (alignment_of).

What is template metaprogramming

Template metaprogramming uses templates to perform computations at compile time, manipulating types and values through template instantiation, specialization, and recursion.

What does std::decay do

std::decay removes reference and cv-qualifiers, converts arrays to pointers, and functions to function pointers — mimicking the way arguments decay when passed by value.

What is std::void_t used for

std::void_t maps any set of types to void. It's a SFINAE helper for detecting whether certain types or expressions are valid in template contexts.

How do I check if a type has a member function

Use a SFINAE trait with void_t and decltype: void_t<decltype(declval<T>().foo())> in a partial specialization.

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