The C++ Textbook

Part 5 · Generic programming

Type traits and metaprogramming

Asking and answering questions about types at compile time.

By the end of this chapter you can

  • Use standard type traits to constrain or branch code
  • Write a trait with a partial specialization
  • Explain if constexpr and why it beats tag dispatch

A template body has to work for every type it is instantiated with, and sometimes those types want different code. A std::string should be moved; an int should be copied. A pointer should be dereferenced before printing; an int should not.

A type trait is how you ask. It is an ordinary class template whose job is to answer a question about a type at compile time — and since the answer is a constant, the previous two chapters’ machinery applies: static_assert it, if constexpr on it, fold it into a constraint.

Asking questions about types
#include <iostream>
#include <string>
#include <type_traits>
#include <vector>

int main() {
    std::cout << std::boolalpha;
    std::cout << "int is integral:            " << std::is_integral_v<int> << '\n';
    std::cout << "double is integral:         " << std::is_integral_v<double> << '\n';
    std::cout << "std::string is trivial:     " << std::is_trivially_copyable_v<std::string> << '\n';
    std::cout << "int* is a pointer:          " << std::is_pointer_v<int*> << '\n';
    std::cout << "vector<int> is same as ...: " << std::is_same_v<std::vector<int>, std::vector<int>> << '\n';
    std::cout << "const int& stripped:        "
              << std::is_same_v<std::remove_cvref_t<const int&>, int> << '\n';
}

Nothing here runs at run time. Every one of those expressions is a bool constant the compiler substituted before main was compiled.

Two shapes: predicates and transformations

The standard traits in <type_traits> come in two kinds, and the naming tells you which is which.

A predicate answers yes or no. It is a class template with a static constexpr bool value, plus a _v variable template alias that saves you writing ::value.

A transformation produces a new type. It has a member typedef type, plus a _t alias that saves you writing typename …::type.

Predicate (_v) Asks
std::is_same_v<A, B> are these the same type?
std::is_integral_v<T>, std::is_floating_point_v<T> which arithmetic family?
std::is_pointer_v<T>, std::is_reference_v<T> indirection?
std::is_base_of_v<B, D> is D derived from B?
std::is_trivially_copyable_v<T> is memcpy a valid copy?
std::is_constructible_v<T, Args...> would T(args...) compile?
Transformation (_t) Gives
std::remove_reference_t<T> T with & or && stripped
std::remove_cvref_t<T> T with references and const/volatile stripped
std::decay_t<T> what by-value deduction would produce
std::conditional_t<B, X, Y> X if B, else Y
std::common_type_t<A, B> the type a ? x : y would have
std::underlying_type_t<E> the integer type behind an enum

if constexpr: branching on a type

Given a trait, you need a way to act on the answer. A plain if is not enough, because both branches of a plain if must compile, for every instantiation.

A plain if compiles both branches
#include <type_traits>

template <class T>
long as_long(T value) {
    if (std::is_pointer_v<T>) return *value;   // compiled even when T is int
    return value;
}

int main() { return static_cast<int>(as_long(42)); }

error: invalid type argument of unary ‘*’ (have ‘int’)

if constexpr (C++17) discards the branch that is not taken, so it is never compiled for that instantiation:

if constexpr discards the branch it does not take
#include <iostream>
#include <string>
#include <type_traits>

template <class T>
void describe(const T& value) {
    if constexpr (std::is_pointer_v<T>) {
        std::cout << "pointer to " << *value << '\n';
    } else if constexpr (std::is_floating_point_v<T>) {
        std::cout << "float-ish " << value << '\n';
    } else if constexpr (std::is_integral_v<T>) {
        std::cout << "integer " << value << " (" << sizeof(T) << " bytes)\n";
    } else {
        std::cout << "something else: " << value << '\n';
    }
}

int main() {
    int n = 7;
    describe(&n);
    describe(2.5);
    describe(n);
    describe(std::string{"text"});
}

The discarded branch is not compiled — but it is still parsed, and any name that does not depend on a template parameter is still checked. if constexpr (false) { this_does_not_exist(); } is an error even though the branch is discarded.

Writing your own trait

The mechanism has no magic in it: a primary template gives the default answer, and a partial specialization gives a different answer for a family of types.

is_vector, from scratch
#include <iostream>
#include <string>
#include <type_traits>
#include <vector>

template <class T>
struct is_vector : std::false_type {};                 // the default: no

template <class E, class A>
struct is_vector<std::vector<E, A>> : std::true_type {};   // any vector: yes

template <class T>
inline constexpr bool is_vector_v = is_vector<T>::value;

int main() {
    std::cout << std::boolalpha
              << is_vector_v<int> << ' '
              << is_vector_v<std::vector<int>> << ' '
              << is_vector_v<std::vector<std::string>> << ' '
              << is_vector_v<std::string> << '\n';
}

std::false_type and std::true_type are just std::integral_constant<bool, false> and …, true> — inheriting from them is how you get the value member without writing it. The specialization matches any std::vector, whatever its element and allocator, because E and A are deduced from the match.

A transformation trait works the same way, but names a type instead:

A transformation trait, and recursion over types
#include <iostream>
#include <type_traits>
#include <vector>

// The element type of a container, or the type itself if it is not one.
template <class T>
struct element_of { using type = T; };

template <class E, class A>
struct element_of<std::vector<E, A>> { using type = typename element_of<E>::type; };

template <class T>
using element_of_t = typename element_of<T>::type;

int main() {
    std::cout << std::boolalpha
              << std::is_same_v<element_of_t<int>, int> << ' '
              << std::is_same_v<element_of_t<std::vector<double>>, double> << ' '
              << std::is_same_v<element_of_t<std::vector<std::vector<char>>>, char> << '\n';
}

The nested case recurses: element_of<vector<vector<char>>> matches the specialization with E = vector<char>, and asks element_of about that. This is the whole of template metaprogramming — a partial specialization is a pattern match, and a nested ::type is a recursive call.

Detecting whether an expression compiles

The most useful traits are not about what a type is but about what you can do with it. Since C++20 that is a requires-expression, and it is worth seeing next to the trait spelling.

Two ways to ask 'does this compile?'
#include <concepts>
#include <iostream>
#include <string>
#include <type_traits>
#include <vector>

// As a concept — the modern spelling.
template <class T>
concept has_size = requires(const T& t) { { t.size() } -> std::convertible_to<std::size_t>; };

// As a trait, using the same machinery the library used before concepts.
template <class T, class = void>
struct sizeable : std::false_type {};

template <class T>
struct sizeable<T, std::void_t<decltype(std::declval<const T&>().size())>> : std::true_type {};

template <class T>
std::size_t length_of(const T& value) {
    if constexpr (has_size<T>) return value.size();
    else                       return sizeof(value);
}

int main() {
    std::cout << std::boolalpha
              << has_size<std::vector<int>> << ' ' << has_size<int> << ' '
              << sizeable<std::string>::value << ' ' << sizeable<double>::value << '\n';
    std::cout << length_of(std::string{"hello"}) << ' ' << length_of(3.0) << '\n';
}

The sizeable trait is the void_t idiom, and it is worth being able to read because the standard library is full of it. The second template parameter defaults to void; the specialization is only a valid match if decltype(…size()) is a valid type, and std::void_t maps whatever that is back to void so the specialization’s argument list matches the default. If the expression is ill-formed the specialization silently drops out — this is SFINAE — and the primary template’s false_type stands.

Write the concept. Read the void_t.

Traits are how a library adapts to your type

The pattern turns up whenever a library needs a fact about a type that the type itself does not provide. std::iterator_traits is the canonical example; here is the shape, reduced.

A library point a user can specialize
#include <iostream>
#include <string>

// The library defines the question and a default answer.
template <class T>
struct printer {
    static void print(const T& value) { std::cout << value; }
};

// A user specializes it for their own type, without touching the library.
struct Money { long cents; };

template <>
struct printer<Money> {
    static void print(const Money& m) {
        std::cout << '$' << m.cents / 100 << '.' << (m.cents % 100 < 10 ? "0" : "")
                  << m.cents % 100;
    }
};

template <class... Args>
void print_all(const Args&... args) {
    ((printer<Args>::print(args), std::cout << ' '), ...);
    std::cout << '\n';
}

int main() {
    print_all(1, std::string{"text"}, Money{12345}, 2.5);
}

This is an extension point. The library never sees Money, and Money never mentions the library; the specialization is the whole of the coupling. It is how std::hash, std::formatter, and std::numeric_limits are extended for user types, and it is the reason those are class templates rather than functions — you cannot partially specialize a function template.

Check yourself

Practice