Type requirements in C++
Template Constraints
Templates in C++, by default, are unconstrained. So it gives us a simple form of static duck-typing. As a simple example, lets look at an increment
and twice
function:
template<class T>
void increment(T& x)
{
++x;
}
template<class T>
void twice(T& x)
{
increment(x);
increment(x);
}
Well, if we call the twice
function with an integer, it will be a number incremented twice:
int i = 1;
twice(i);
std::cout << i; // Prints 3
Now since the template parameters are unconstrained if we pass a type that in not an integer.
foo f;
twice(f);
We will get a compile error like this:
twice.cpp:6:5: error: cannot increment value of type 'foo'
++x;
^ ~
twice.cpp:12:5: note: in instantiation of function template specialization 'increment<foo>' requested here
increment(x);
^
twice.cpp:25:5: note: in instantiation of function template specialization 'twice<foo>' requested here
twice(f);
^
However, this error will occur in the increment function(with a backtrace to our twice function call) and not at the call to twice
. Of course in this simple example its easy to see whats wrong, but if twice
was library code its unclear if the error is due to a mistake in the library or whether its a mistake made by the user. So we should specify type requirements for each of our template parameters, and then add constraints to these template parameters.
For now, we will require the types to be integers, so we can use std::enable_if
to constraint the template parameter(which uses something called subtitution failure to constrain the template). There are several different forms to use std::enable_if
depending on the context. The most common form is to use a default template parameter, like this:
template<class T, typename std::enable_if<(std::is_integral<T>()), int>::type = 0>
void increment(T& x)
{
++x;
}
Now, since there is lot of noise to enable_if
, it is better to make a little macro to improve readability:
#define REQUIRES(...) typename std::enable_if<(__VA_ARGS__), int>::type = 0
template<class T, REQUIRES(std::is_integral<T>())>
void increment(T& x)
{
++x;
}
template<class T, REQUIRES(std::is_integral<T>())>
void twice(T& x)
{
increment(x);
increment(x);
}
So this uses the std::is_integral
type trait to check that the type is an integer. So now if we try to compile the previous code:
foo f;
twice(f);
We will get a compile error like this:
twice.cpp:23:5: error: no matching function for call to 'twice'
twice(f);
^~~~~
twice.cpp:11:19: note: candidate template ignored: disabled by 'enable_if' [with T = foo]
template<class T, REQUIRES(std::is_integral<T>())>
^
Which first points the error in the user code, and then shows the type requirements for the function.
Custom Type Trait
So we used std::is_integral
type trait, however this is too restrictive. The increment
could work on floating points and pointers. We could create a custom type trait using std::integral_constant
like this:
template<class T>
struct is_incrementable
: std::integral_constant<bool, (
std::is_integral<T>() &&
std::is_floating_point<T>() &&
std::is_pointer<T>())>
{};
However, still that doesn’t cover everything. Instead we can create a trait and constrain it by a valid expression. First we create the trait as false with a default template parameter:
template<class T, class Enable=void>
struct is_incrementable
: std::false_type
{};
Now the Enable
parameter is what we will use to check for a valid expression. So we specialize is_incrementable
, but we pass in always_void
for the specialization of the Enable
parameter(its void
because that is what we set the default parameter to). The always_void
template will allow us to put expressions that we want to check for:
template<class T>
struct always_void
{
typedef void type;
};
template<class T>
struct is_incrementable<T, typename always_void<
decltype(++std::declval<T&>())
>::type>
: std::true_type
{};
So if decltype(++std::declval<T&>())
is not a valid exression the compiler will pick the default definition. However, there is a lot of boilerplate involved in setting this up, plus its a little ugly to read with all the std::declval
. Instead, we can take advantage of the fact the we can constrain a function using a trailing decltype
(from Eric Niebler’s blog post here), like this:
struct Incrementable
{
template<class T>
auto requires_(T&& x) -> decltype(++x);
};
So if ++x
is not valid, then the requires_
member function is not callable. So we can create a models
trait that just checks if requires_
is callable:
template<class Concept, class Enable=void>
struct models
: std::false_type
{};
template<class Concept, class... Ts>
struct models<Concept(Ts...), typename always_void<
decltype(std::declval<Concept>().requires_(std::declval<Ts>()...))
>::type>
: std::true_type
{};
This uses the function signature syntax for a template so it will work with any number of paramenters to the requires_
function. So using the models
trait we have simple way to define and check for type requirements.
So now we can define our is_incrementable
trait like this:
template<class T>
struct is_incrementable
: models<Incrementable(T)>
{};
Or we could just use the models
trait in our requires clause like this:
template<class T, REQUIRES(models<Incrementable(T)>())>
void increment(T& x)
{
++x;
}
template<class T, REQUIRES(models<Incrementable(T)>())>
void twice(T& x)
{
increment(x);
increment(x);
}