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.
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.
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).
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
A
and B
are equivalent (one should
hope not)A
and A
are equivalent (one should
hope so)A*
decays into A
A&
decays into A
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.