10/1/2015
Template metaprogramming has to be one of the coolest ways of abstracting your code. It is very similar to polymorphism because it is polymorphism! The only difference is that template metaprogramming happens at compile time rather than runtime time. Template metaprogramming can be used to mimic functional programming languages or to build abstract ways of using generic code.
So why do we need template metaprogramming? The real reason is that we want to remove stupid duplicate code. Java allows you to use polymorphism and generic programming but these methods only work on classes and not on builtin data types. I implemented a small example in to show how code repetition is bad (underlined are the only changes):
int max(int x, int y) { return x > y ? x : y; } float max(float x, float y) { return x > y ? x : y; } double max(double x, double y) { return x > y ? x : y; }
As you can see, we have a lot of uninteresting code that is honestly pretty useless. C++ allows us to abstract over this using a template. This template will work on any type that implements the greater than operator!
template<class T> T max(const T& x, const T& y) { return x > y ? x : y; }
Don't be fooled when I write class T
! It
works perfectly well
with int
s, float
s, etc!
Since the template is resolved at compile time, you
can even create specializations of a function that will
act differently! Let's take all integral types
(char
, int
, double
etc) as values rather than references: (note that this
only works in C++11 and beyond because of the time that
the standard library incorporated it in.)
#include <type_traits> template<class T> typename std::enable_if<!std::is_arithmetic<T>::value, T>::type max(const T& x, const T& y) { return x > y ? x : y; } template<class T> typename std::enable_if<std::is_arithmetic<T>::value, T>::type max(T x, T y) { return x > y ? x : y; }
Let's look at the line typename
std::enable_if<std::is_arithmetic<T>::value,
T>::type
. The std::is_arithmetic<T>
part creates a class at compile time that has one field:
value. This field is true if the type given is an
arithmetic type (int
, float
,
char
, bool
, double
,
unsigned long
, etc.), and false if it is not.
The ::value
then will extract this bool.
This means that the std::enable_if<...,
T>::type
line tells the compiler to only use
this implementation only if the bool given in the first
parameter is true. Then the ::type
extracts
the T
from it if it is true and allows you to
use that as the return type by
putting typename
before it! All of this
functionality is not builtin to the language, allowing you
to make your own methods that have their own
specifications.
The following code will swap two references of any type implementing a copy assignment operator and copy constructor.
template<class T> void swap(T& a, T& b) { T temp = a; a = b; b = temp; }
A lot of really cool things are possible with template metaprogramming! I suggest watching these videos if you are interested in learning more. Before I end it though, I will show you a side by side comparison of template meta programming with interfaces in C++.
class incrementable { virtual incrementable& operator++() = 0; virtual incrementable& operator--() = 0; }; class ex_1 : public incrementable { // private by default! int i; public: incrementable& addOne() { i++; return *this; } incrementable& subOne() { i--; return *this; } }; void add_five_times(incrementable& a) { for(int i = 0; i < 5; i++) { // use prefix with classes! ++a; } }
This method works fine for basic operations but breaks down if you want to intersperse classes and arithmetic types. Let's implement something similar using templates.
#include <type_traits> template<class T, class = void> struct incrementable : std::false_type { }; template<class T> struct incrementable <T, typename std::enable_if< std::is_same<T&, decltype(++std::declval<T>())>::value && std::is_same<T&, decltype(--std::declval<T>())>::value >::type > : std::true_type { }; template<class T> struct add_able<T, typename std::enable_if<std::is_arithmetic<T>::value>::type> : std::true_type { }; template<class T> typename std::enable_if< incrementable<T>::value, void >::type add_five_times(T& a) { for(int i = 0; i < 5; i++) ++a; }
The second and third lines dictate the base case: if none
of the other more specialized cases match, then we extend
false_type: a class that aliases false
to the
value type
. This means that we only
get type
as true
if the absurd
case occurs: the class fits our interface.
Now we have to define under what situations we will have
a true_type
. The second struct
initialization will only trigger if the class defines a
prefix ++
and --
operator and
both of those operators return a reference to the class.
enable_if
only defines the type
member if the condition is true, meaning that the code
won't evaluate at all if it doesn't fit the description of
the interface.
The third case will enact on any type that is considered
arithmetic: int
, float
,
bool
, char
, long
,
etc. For some reason they don't work with the second
condition but they should still be considered
incrementable.
We then use incrementable by declaring the return type as void if the type is incrementable, and not implementing the function at all if it isn't incrementable. This new function works over all types that worked on the interface example as well as all numeric types! All of this happens at compile time, so there is also no overhead of a virtual function.
As you have seen, template metaprogramming can be very complicated, but it allows you to generalize your code more and push polymorphism to compile time rather than runtime.