Effective C++ item 48: Be Aware of Template Metaprogramming

Template metaprogramming(TMP) is the process of writing template-based C++ programs that execute during compilation. A template metaprogram is a program written in C++ that executes inside the C++ compiler. When a TMP program finishes running, its output — pieces of C++ source code instantiated from templates — is then compiled as usual.

TMP has two great strengths. First, it makes some things easy that would otherwise be hard or impossible. Second, because template metaprograms execute during C++ compilation, they can shift work from runtime to compile-time.

It is remarked earlier that some things are easier in TMP than in “normal” C++, and advance offers an example of that, too. Item 47 mentions that the typeid-based implementation of advance can lead to compilation problems, and here’s an example where it does:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void advance(std::list<int>::iterator& iter, int d)
{
if (typeid(std::iterator_traits<std::list<int>::iterator>::iterator_category) ==
typeid(std::random_access_iterator_tag)) {

iter += d; // <----- compilation error!!!
}
else {
if (d >= 0) { while (d--) ++iter; }
else { while (d++) --iter; }
}
}

int main() {

std::list<int>::iterator iter;
advance(iter, 10);
return 0;
}

Above code won’t compile. In this case, we’re trying to use += on a list<int>::iterator, but list<int>::iterator is a bidirectional iterator (see Item 47), so it doesn’t support +=. Only random access iterators support +=. Now, we know we’ll never try to execute the += line, because the typeid test will always fail for list<int>::iterators, but compilers are obliged to make sure that all source code is valid, even if it’s not executed, and iter += d isn’t valid when iter isn’t a random access iterator. Contrast this with the traits-based TMP solution, where code for different types is split into separate functions, each of which uses only operations applicable to the types for which it is written.

TMP has been shown to be Turing-complete, which means that it is powerful enough to compute anything. Using TMP, you can declare variables, perform loops, write and call functions, etc. But such constructs look very different from their “normal” C++ counterparts. For example, Item 47 shows how if...else conditionals in TMP are expressed via templates and template specializations.

Now let’s look at how TMP does loops. TMP loops don’t involve recursive function calls, they involve recursive template instantiations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
template<unsigned n>
struct Factorial {
enum { value = n * Factorial<n-1>::value };
};
template<>
struct Factorial<0> {
enum { value = 1 };
};
int main() {
std::cout << Factorial<0>::value << std::endl;
std::cout << Factorial<1>::value << std::endl;
std::cout << Factorial<2>::value << std::endl;
std::cout << Factorial<3>::value << std::endl;
std::cout << Factorial<4>::value << std::endl;
return 0;
}

The looping part of the code occurs where the template instantiation Factorial<n> references the template instantiation Factorial<n-1>. Like all good recursion, there’s a special case that causes the recursion to terminate. Here, it’s the template specialization Factorial<0>.

TMP is not for everybody, but TMP support in c++ language is on the rise, especially if you look at c++17 and beyond. To grasp why TMP is worth knowing about, it’s important to have a better understanding of what it can accomplish. Here are three examples:

  • Ensuring dimensional unit correctness.
  • Optimizing matrix operations.
  • Generating custom design pattern implementations.

Reference:
“Effective C++” Third Edition by Scott Meyers.