Template Metaprogramming Part 1
By on January 14, 2017
What is template metaprogramming? Why do I care about template metaprogramming? What is a typelist? These are the sorts of questions I asked myself when I first started looking at template metaprogramming seriously.
In this series of posts I will do my best to answer these questions using practical (from my line of work, anyway) examples that will hopefully convince you of the merits of template metaprograming (TMP). I will try to answer the first two questions together. TMP is writing code that is executed by the compiler and manipulates types rather than data. TMP is even more powerful when combined with constant expressions to do compile-time computations. I will restrict myself to using C++11 since this is currently best supported on the HPC systems used in academic settings. Currently the Intel compiler is what is prohibiting us from fully adopting C++14.
I will begin by discussing several runtime ideas that we need to
implement at compile time in order to be able to have flexible
metacode. TMP is functional programming in the sense that all
metavariables or types and type aliases are
immutable
(const) and that all metafunctions return
by value. In order to have flexible and useful metaprograms we want
the idea of if, for and while, so let’s start with if. I will
cover loops in part 2. How do
we implement an if in TMP? Well we need a bool that can be true
or false, so that could be one parameter to the metafunction. We also
need the value that is returned if the bool is true and the return
value if the bool is false. Okay, so our metafunction will need to take
three parameters, so let’s start with that.
1
2
3
4
5
6
7
8
template <bool B, typename T, typename F>
struct if_c {
using type = F;
};
template <typename T, typename F>
struct if_c<true, T, F> {
using type = T;
};
What is happening here? We use a struct as the metafunction that
applies the if and call it if_c (recall that if is a
keyword). Arguments are passed to the metafunction as template
parameters, and returned by setting a member type alias or static
constexpr variable. For if_c the general (unspecialized) definition
takes three template parameters: a bool, the metavalue (type) to be
returned if the bool is true and the type to be returned if the bool
is false. The general definition sets its member
metavariable type to the false evaluation and we provide a (partial)
template specialization on line 5 where the bool is true. The
specialization sets the metavariable type to the true evaluation
template parameter. How does this actually perform a conditional
check? Well the compiler must use a specialization of if_c if it
exists, if not
it attempts to instantiate and use the general definition. Thus, if
the first template parameter is true then the specialization is
used, otherwise the general definition is used. Why did we choose to
make the specialization for the true case? This is an arbitrary
choice (and is the same one made in libc++). What we have implemented
here is
actually already in the standard library (stdlib, either libc++ or
stdlibc++) and is named std::conditional.
Okay, great, we’re done with a compile time if! Wait, not so fast!
Sometimes in runtime code we use if statements to avoid evaluating
one branch altogether but that isn’t what happens with
conditional. When using conditional all template parameters must
be evaluable, which isn’t always what we need. Furthermore, maybe we
want to “branch” compilation depending on a conditional or simply
avoid the compiler checking a branch altogether so our compile time
stays low. By branch I mean maybe we want a function that has the
same name but operates on
either associative containers (std::map or
std::unordered_map)
or sequence containers (std::array or
std::vector). Well how do we
do this? Clearly conditional isn’t the answer here, what we want is
something that enables a (meta)function if a condition is met. This is
exactly what std::enable_if does.
Let’s now implement this second type of if statement. Here is our
implementation:
1
2
3
4
5
6
template <bool B, typename T = void>
struct enable_if {};
template <typename T>
struct enable_if<true, T> {
using type = T;
};
While shorter than conditional it is more subtle in its
utility. First enable_if takes two template parameters, a bool and a
type where the type is void by default and really serves as the method
of enabling a (meta)function. Note that the template parameter T has
a default value of void for all specializations too. Just like arguments
to functions may only have their default value specified
once, template parameters may only have their default value
specified once. We again have the general definition handle the
false case and supply a specialization for the true case with a
member metavariable type set to the template parameter T (void by
default). So how do we actually use enable_if and how does it work?
Well let’s first look at a practical use and then I’ll explain how
enable_if works.
As our example let’s write a few type traits to check if a type is a
map or unordered_map. The concepts are the same as what we used
for conditional and enable_if and plays a central role in TMP. We
first define a generic class template that inherits from
std::false_type and therefore has a member
variable named value whose value is (hopefully not surprisingly)
false. Next we define a template specialization that is
specialized for a map and inherits from
std::true_type. This works because the
compiler has to always choose the specialization if it can, just as
with conditional and enable_if where the compiler selects the
true specialization. Here are the implementations:
1
2
3
4
5
6
7
8
9
10
11
12
template <typename T>
struct is_std_map : std::false_type {};
template <typename K, typename V, typename Comp, typename A>
struct is_std_map<std::map<K, V, Comp, A>> : std::true_type {};
template <typename T>
struct is_std_unordered_map : std::false_type {};
template <typename K, typename V, typename Hash, typename KeyEqual, typename A>
struct is_std_unordered_map<std::unordered_map<K, V, Hash, KeyEqual, A>>
: std::true_type {};
You may find it surprising that the specialization has more template
parameters than the general definition. This is fine because the
<std::map<K, V, Comp, A>> after the struct name on line 5 indicates
that we have defined a specialization. Thus the compiler knows to
first use this specialized definition if it is satisfied. Finally,
what we have is that
is_std_map<std::map<int, double>>::value is true while
is_std_map<std::unordered_map<int, double>>::value is false. The
implementation for unordered_map is essentially the same.
Since both map and unordered_map have the same dereferenced
iterator (a pair) we might only care if the type is either a map
or unordered_map but not which one. Let’s write another type trait
that checks if a type is either map or unordered_map and call it
is_map. Again the
general definition will need the type to check as a parameter, and
inherit from false_type. Now how do we use our above type traits to
specialize is_map? We use enable_if, that’s how! In order to use
enable_if we must add a second template parameter that we will not
use other than to perform the enable_if check. Therefore we can use
an unnamed template parameter with a default value, which we’ll set to
void. The reason for setting a default value is so that the user of
our type trait does not need to specify a second “filler” template
parameter. Let me show you the general definition and specialization
together, then explain the specialization.
1
2
3
4
5
6
template <typename T, typename = void>
struct is_map : std::false_type {};
template <typename T>
struct is_map<T, typename std::enable_if<is_std_map<T>::value or
is_std_unordered_map<T>::value>::type>
: std::true_type {};
This might seem like a bit to swallow, but let’s break it down. The
specialization only takes one template parameter, the type T that we
want to check. The second parameter holds the enable_if
statement, which in this case takes the result of a logical or
between the result of is_std_map<T>::value and
is_std_unordered_map<T>::value. If T is either a map or an
unordered_map then the specialization of enable_if is used and it
has a member metavariable type. In this case the compiler can
compile the code and must prefer the specialization over the general
definition. If the result of the logical or is false then the
general definition of enable_if is used and the compiler cannot
compile the code because the general enable_if does not have a
metavariable named type. As a result the compiler ignores the
specialization of is_map and falls back to the generic
definition. Lastly, the typename keyword is required to tell the
compiler that type is a metavariable (a type) rather than a
variable with a value.
So what have we discovered? Well we discovered that there are at least
three different ways to perform if-else or switch statements at
compile time. One is using std::conditional,
another is using std::enable_if, and lastly we
can also use (partial) template specialization to have the compiler
select a certain definition over all others. Comparing these to
runtime concepts conditional is probably most like the ternary
operator, enable_if like if-else statements, and (partial) template
specialization like if-else and switch statements.