Traits #
A particularly useful trait in numerics is numerical_limits. We can use it as
follows:
double eps = std::numeric_limits<double>::lowest();
size_t magic_value = std::numeric_limits<size_t>::max();
What makes numeric_limits interesting is how it provides useful information
about common types. We’ll call a template class who’s purpose is to provide a
uniform API for semantically related, but otherwise “incompatible” types, a
trait. We’ll continue to explain how to write our own traits; and why we want
to do so.
Imagine we’d like to generalize the following algorithm:
double minimum(const std::vector<double> &values) {
double xmin = std::numeric_limits<double>::inf();
for (auto x : values) {
xmin = std::min(xmin, x);
}
return xmin;
}
We’d start by making it a template:
template<class T>
T minimum(const std::vector<T> &values) {
T xmin = std::numeric_limits<T>::infinity();
for (auto x : values) {
xmin = std::min(xmin, x);
}
return xmin;
}
which will fail for minimum<size_t>. Hence, we want to be able to allow the
user to customize the initial value of xmin.
Attempt I: Brute-force Trait. #
We’ll use a trait to do so:
template <class T>
struct minimum_traits;
We could write one specialization for size_t …
template <>
struct minimum_traits<size_t> {
static int initialization_value() { return std::numeric_limits<size_t>::max(); }
};
… and another for double.
template <>
struct minimum_traits<double> {
static double initialization_value() {
return std::numeric_limits<double>::infinity();
}
};
We could now replace one line mininum with the following:
double xmin = minimum_traits<T>::initialization_value();
and call it quits. However, when someone also needs int and float, we’d
need to implement both.
Attempt II: Generic Trait. #
Attempt I failed because we needed to write one specialization per type. That’s too much repetition; and we can logically group all integer types and all floating point numbers together.
Let’s start over. This time is has two template parameters. One for the type; and another to play tricks with the compiler (we didn’t bother giving it an name).
template <class T, class = void>
struct minimum_traits;
It would be convenient if we could tell the compiler to ignore certain
definitions, because that would allow us to write one version of minimum<T, void> for integers, which isn’t visible for floats; and another for floating
point numbers which isn’t visible for integers. This will allow us to write
code that invalid for floating point numbers in the body of the version for
integers and vice-versa.
There is a way, namely by writing code that has what looks like a bug. If that bug happens in one of a few, very particular locations then it falls under Substitution Failure Is Not An Error (SFINAE). Crucially, this mean that “the bug” doesn’t result in a compiler error and instead that specialization will be ignored.
Unfortunately, the body of the method is not covered by SFINAE. Instead we use a second template argument to create a place were we can trigger substitution failures.
The common way to create such a failure is std::enable_if. Which could be
defined as follows:
template<bool cond, class T = void>
struct enable_if;
template<class T = void>
struct enable_if<true, T> {
using type = T;
};
template<bool cond, class T>
struct enable_if<false, T> {
// nothing here
};
We create a compilation error as follows:
enable_if</* cond */>::type
Let’s return to the minimum_traits and try again:
template <class T>
struct minimum_traits<T, typename std::enable_if<std::is_integral_v<T>>::type> {
static T initialization_value() { return std::numeric_limits<T>::max(); }
};
we’d do the same for floating point numbers. However, if the condition is more complex or we’ll be using it multiple times we could consider the following:
template <class T, class U = void>
struct enable_if_floating_point : std::enable_if<std::is_floating_point_v<T>> {
};
and then:
template <class T>
struct minimum_traits<T, typename enable_if_floating_point<T>::type> {
static T initialization_value() { return std::numeric_limits<T>::infinity(); }
};