By Brian Sohr, Principal Software Developer
In previous entries, I have examined a handful of small C++ techniques, such as lambda expressions and type traits. Continuing with the C++ theme, I am going to attempt to give a brief example/introduction to using template metaprogramming. I don't often employ metaprogramming tricks, but knowing about them and having them in your skillset can sometimes prove handy.
To aid in the explanation, I'm just going to cover two examples (both covered on modernescpp.com)- one is perhaps the most widely known example of metaprogramming, and another that is more of a modern illustration on its utility.
We should probably begin with a short description of what metaprogramming is. As the name implies, metaprogramming is essentially executing a program ON a program. That is, it's essentially execution at compile-time, leveraging the compiler's activities as it resolves templated code.
This starts to become a bit more illuminated when we look at the first example, presented in the mid-1990s by Erwin Unruh of Siemens at a conference. The lore is that, while the code didn't compile, the compiler output was sort of a fascinating accident. Here is the code, and the corresponding original compiler errors (from modernescpp.com):
// Prime number computation by Erwin Unruh
template <int i> struct D { D(void*); operator int(); };
template <int p, int i> struct is_prime {
enum { prim = (p%i) && is_prime<(i > 2 ? p : 0), i -1> :: prim };
};
template < int i > struct Prime_print {
Prime_print<i-1> a;
enum { prim = is_prime<i, i-1>::prim };
void f() { D<i> d = prim; }
};
struct is_prime<0,0> { enum {prim=1}; };
struct is_prime<0,1> { enum {prim=1}; };
struct Prime_print<2> { enum {prim = 1}; void f() { D<2> d = prim; } };
#ifndef LAST
#define LAST 10
main () {
Prime_print<LAST> a;
}
And the corresponding original compiler output supplied by Unruh (I've somewhat condensed it):
MetaWare High C/C++ Compiler R2.6
(c) Copyright 1987-94, MetaWare Incorporated
E "primes.cpp",L16/C63(#416): prim
| Type `enum{}´ can´t be converted to txpe `D<2>´ ("primes.cpp",L2/C25).
E "primes.cpp",L11/C25(#416): prim
| Type `enum{}´ can´t be converted to txpe `D<3>´ ("primes.cpp",L2/C25).
E "primes.cpp",L11/C25(#416): prim
| Type `enum{}´ can´t be converted to txpe `D<5>´ ("primes.cpp",L2/C25).
E "primes.cpp",L11/C25(#416): prim
| Type `enum{}´ can´t be converted to txpe `D<7>´ ("primes.cpp",L2/C25).
E "primes.cpp",L11/C25(#416): prim
| Type `enum{}´ can´t be converted to txpe `D<11>´ ("primes.cpp",L2/C25).
E "primes.cpp",L11/C25(#416): prim
| Type `enum{}´ can´t be converted to txpe `D<13>´ ("primes.cpp",L2/C25).
E "primes.cpp",L11/C25(#416): prim
| Type `enum{}´ can´t be converted to txpe `D<17>´ ("primes.cpp",L2/C25).
E "primes.cpp",L11/C25(#416): prim
| Type `enum{}´ can´t be converted to txpe `D<19>´ ("primes.cpp",L2/C25).
E "primes.cpp",L11/C25(#416): prim
| Type `enum{}´ can´t be converted to txpe `D<23>´ ("primes.cpp",L2/C25).
E "primes.cpp",L11/C25(#416): prim
| Type `enum{}´ can´t be converted to txpe `D<29>´ ("primes.cpp",L2/C25).
So, why did Mr. Unruh find this "broken" program so fascinating? As you probably observed immediately, amid its complaining, the compiler had identified the first ten prime numbers. While unable to execute the program, the compiler in fact ran an almost equivalent routine. Kind of interesting, right? A similar, perhaps more famous example that can be easily found is computing factorial at compile time.
So, while this is kind of cool, and a watershed moment of sorts, it's unlikely that it is very applicable or useful in anything we might write in our actual work. It does, however, introduce the computational facilities that we can leverage in the compiler. For something that might be of more utility, we can examine the more traditional compile-time type-manipulation.
In a past entry, I looked at some of the compile-time tools provided by the type traits library in C++. One of these, std::remove_const<T>, does just exactly that - removes top-level const qualification on a type. So, essentially:
typedef const int c_int_t;
std::remove_const<c_int_t>::type my_int; // my_int is an int, rather than const int
Simple enough, correct? I found an implementation of this behavior on modernescpp.com, and thought it would serve as a nice example of this family of techniques:
// removeConst.cpp
#include <iostream>
#include <type_traits>
template<typename T >
struct removeConst {
using type = T; // (1)
};
template<typename T >
struct removeConst<const T> {
using type = T; // (2)
};
int main() {
std::cout << std::boolalpha;
std::cout << std::is_same<int, removeConst<int>::type>::value << '\n'; // true
std::cout << std::is_same<int, removeConst<const int>::type>::value << '\n'; // true
}
This is quite simple, as it's pretty evident what's going on here - in the case that a const type is given as the template argument, the lower partial specialization is used, in the non-const case, the trivial, almost pass-through counterpart at the top is used. While almost so simple as to seem pointless or silly, just being aware of these sorts of "tricks" can be very useful as you write more generic code.
Hopefully this provides a little context as to what metaprogramming is and how it might be of use in certain settings. As I mentioned at the outset, I don't have occasion to use it frequently, but it has proved useful when writing, fixing, or extending code with generic elements. These examples are both from modernescpp.com, which provides some excellent and thorough articles describing these techniques more robustly.