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.
#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.
#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:
#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.
#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:
#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.
#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.
#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.