Home

Playing with C++ templates

Last week I found myself quite sick. Rather than wallow in it, I played around with C++ templates. Until now, I’ve only parameterized classes into generic functions. As a daily user of folly, I’ve spent quite some time puzzling over C++ template magic. It’s time I got my feet wet.

Simple example

We want a generic function to call a standard interface:

#include <iostream>

struct A {
    int a() {
        return 1;
    }
};

struct B {
    int a() {
        return 2;
    }
};

template <typename T>
void foo() {
    T t;
    std::cout << t.a() << "\n";
}

int main() {
    foo<A>();
    foo<B>();
}

When compiled and run:

$ g++ typename.cpp
$ ./a.out
1
2

Note that we may choose to use old-style class instead of typename on line 15. However, for clarity, class should be avoided. Trivia: Bjarne originally reused the class keyword in template declarations to avoid another reserved keyword.

Integers inside templates

Now another example. Did you know templates declarations can specify more than class types? In fact, according to the cppreference, we can use:

Let’s play around with an integer type inside a template:

#include <iostream>

template <int N = 10>
void foo() {
    std::cout << N << "\n";
}

int main() {
    foo<1>();
    foo<2>();
    foo();
}

When compiled and run:

$ g++ int.cpp
$ ./a.out
1
2
10

One can see how this might be useful to flexibly declare static array sizes at compile time (to avoid variable-length arrays, whose ugliness is described in last week’s LWN).

Template metaprogramming

Now that we’ve covered some template basics, let’s play around with template metaprogramming. Succinctly, template metaprogramming is the ability to do fancy compile time things. For example, a library developer can optimize a templated routine for certain template parameter types. One way to do this is through std::enable_if. In folly, the futures library overloads onError(F&& func) to return a different return type based on the template parameter type.

We will do something far less complicated. We will play with std::decay, a C++ standard library function that “decays” types. For example, the type Foo& and Foo should, in many cases, be considered the same. However, a C++ compiler cannot safely assume that is always the case, especially for functions like std::is_same.

In the spirit of flexibility, the C++ committee provides a mechanism to “decay” types into “base” types. Hopefully that makes more sense than the official documentation:

Applies lvalue-to-rvalue, array-to-pointer, and function-to-pointer implicit conversions to the type T, removes cv-qualifiers, and defines the resulting type as the member typedef type.
#include <iostream>
#include <type_traits>

struct A {};
struct B : A {};

int main() {
    std::cout << std::boolalpha
        << std::is_same<A, B>::value << '\n'
        << std::is_same<A, A>::value << '\n'
        << std::is_same<A, std::decay<A*>::type>::value << '\n'
        << std::is_same<A, std::decay<A&>::type>::value << '\n';
}

While seemingly complicated, this snippet is quite simple. We perform 4 checks on lines 9-12: if

When compiled and run:

$ g++ decay.cpp
$ ./a.out
false
true
false
true

Hopefully checks 1 and 2 are not surprising. Checks 3 and 4 deserve a brief explanation:

Check 3 shows that A* does not decay into A. This makes sense because at runtime, one cannot perform the same operations on A* as on A. For instance, if A were a class, to call a method on type A, one does A.foo(). One cannot do the same with A*. A* needs to be dereferenced first.

Check 4 shows that A& decays into A. This makes sense because at runtime, most non-metaprogramming operations work the same.