792 Metaprogramming Chapter 28 The general version of select

792
Metaprogramming
Chapter 28
class Nil {};
template<int I, typename T1 =Nil, typename T2 =Nil, typename T3 =Nil, typename T4 =Nil>
struct select;
template<int I, typename T1 =Nil, typename T2 =Nil, typename T3 =Nil, typename T4 =Nil>
using Select = typename select<I,T1,T2,T3,T4>::type;
// Specializations for 0-3:
template<typename T1, typename T2, typename T3, typename T4>
struct select<0,T1,T2,T3,T4> { using type = T1; }; // specialize for N==0
template<typename T1, typename T2, typename T3, typename T4>
struct select<1,T1,T2,T3,T4> { using type = T2; }; // specialize for N==1
template<typename T1, typename T2, typename T3, typename T4>
struct select<2,T1,T2,T3,T4> { using type = T3; }; // specialize for N==2
template<typename T1, typename T2, typename T3, typename T4>
struct select<3,T1,T2,T3,T4> { using type = T4; }; // specialize for N==3
The general version of select should never be used, so I didn’t define it. I chose zero-based numbering to match the rest of C++. This technique is perfectly general: such specializations can
present any aspect of the template arguments. We don’t really want to pick a maximum number of
alternatives (here, four), but that problem can be addressed using variadic templates (§28.6). The
result of picking a nonexisting alternative is to use the primary (general) template. For example:
Select<5,int,double,char> x;
In this case, that would lead to an immediate compile-time error as the general Select isn’t defined.
A realistic use would be to select the type for a function returning the Nth element of a tuple:
template<int N, typename T1, typename T2, typename T3, typename T4>
Select<N,T1,T2,T3,T4> get(Tuple<T1,T2,T3,T4>& t);
// see §28.5.2
auto x = get<2>(t); // assume that t is a Tuple
Here, the type of x will be whatever T3 is for the Tuple called t. Indexing into tuples is zero-based.
Using variadic templates (§28.6), we can provide a far simpler and more general select:
template<unsigned N, typename... Cases>
struct select;
// general case; never instantiated
template<unsigned N, typename T, typename... Cases>
struct select<N,T,Cases...> :select<N−1,Cases...> {
};
Section 28.3.1.3
template<typename T, typename... Cases>
struct select<0,T,Cases...> {
using type = T;
};
Selecting among Several Types
793
// final case: N==0
template<unsigned N, typename... Cases>
using Select = typename select<N,Cases...>::type;
28.3.2 Iteration and Recursion
The basic techniques for calculating a value at compile time can be illustrated by a factorial function template:
template<int N>
constexpr int fac()
{
return N∗fac<N−1>();
}}
template<>
constexpr int fac<1>()
{
return 1;
}
constexpr int x5 = fac<5>();
The factorial is implemented using recursion, rather than using a loop. Since we don’t have variables at compile time (§10.4), that makes sense. In general, if we want to iterate over a set of values at compile time, we use recursion.
Note the absence of a condition: there is no N==1 or N<2 test. Instead, the recursion is terminated when the call of fac() selects the specialization for N==1. In template metaprogramming (as in
functional programming), the idiomatic way of working your way through a sequence of values is
to recurse until you reach a terminating specialization.
In this case, we could also do the computation in the more conventional way:
constexpr int fac(int i)
{
return (i<2)?1:fac(i−1);
}
constexpr int x6 = fac(6);
I find this clearer than the function template expression of the idea, but tastes vary and there are
algorithms that are best expressed by separating the terminating case from the general case. The
non-template version is marginally easier for a compiler to handle. The run-time performance will,
of course, be identical.
The constexpr version can be used for both compile-time and run-time evaluation. The template
(metaprogramming) version is for compile-time use only.