f you've been programming in C++ for any time at all then you're familiar with variadic functions, functions (such as printf) that can take a variable number of arguments. C99 introduced variadic macros, which also take a variable number of arguments. C++0x takes this concept a step further with the introduction of variadic templates, where the number of template arguments is not specified when you write the template.
You declare the variadic part of a variadic template with an ellipsis (...) just like with a variadic function, though in this case it goes in the template parameter list:
template<typename ... Args>
class my_class
{};
You can then specify the arguments when you use the template. This approach is the same one you would use for a normal template, except that you can specify as many or as few arguments as you like:
my_class<int> mc1;
my_class<double,char,std::string> mc2;
Just like with variadic functions, you don't even have to pass any arguments:
my_class<> mc3;
You can have other non-variadic template parameters too. After arguments have been allocated to the non-variadic parameters, the remainders constitute the parameter pack for the variadic parameter:
template<typename T,typename U,typename ... Args>
class x
{};
x<int,char,double,std::string> x1; // Args is <double,std::string>
x<std::string,my_class<int> > x2; // Args is empty
template<typename T>
void print_comma_separated_list(T value)
{
std::cout<<value<<std::endl;
}
template<typename First,typename ... Rest>
void print_comma_separated_list(First first,Rest ... rest)
{
std::cout<<first<<",";
print_comma_separated_list(rest...);
}
You can then call this with a list of values of any type that can be written to std::cout:
print_comma_separated_list(42,"hello",2.3,'a');
How does this work? If there's only one element, you should just print that on it's own with a trailing newline. You take care of that with the first overload. If you've got more than one element to print, then you need to separate them with commas. That's where the variadic function template comes in.
The use of the ellipsis in the declaration of the function parameter "rest" is what's called a pack expansion, which means that "rest" is actually a pack of function parameters. It has one parameter for each element in the template parameter pack "Rest".
The ellipsis in the recursive call expands "rest" into a series of values, which are passed as normal arguments to the recursive call. This works both ways: if you pass more than one argument to print_comma_separated_list, the first argument is used to deduce "First" and "Rest" is deduced from the remaining arguments. Thus, in this code:
print_comma_separated_list(42,"hello",2.3,'a');
"First" is deduced to be int, and "Rest" is deduced to be <const char*,double,char>.
So far, so good. Now let's move on to the body of the function.
print_comma_separated_list(42,"hello",2.3,'a');
print_comma_separated_list("hello",2.3,'a');
print_comma_separated_list(2.3,'a');
print_comma_separated_list('a');
This final call will match the single-argument overload you defined at the beginning and will therefore terminate the recursion. Unfortunately, as written this call will copy all the parameters except the first with every recursive call. If you have twenty parameters then the last one will be copied twenty times!
Thankfully, you can avoid that by using rvalue references:
template<typename T>
void print_comma_separated_list(T&& value)
{
std::cout<<value<<std::endl;
}
template<typename First,typename ... Rest>
void print_comma_separated_list(First&& first,Rest&& ... rest)
{
std::cout<<first<<",";
print_comma_separated_list(rest...);
}
In this case, the template type deduction rules mean that if lvalues (such as named variables) are passed to the function, then the template argument "First" or the corresponding element of "Rest" is deduced to be an lvalue reference. This reference is then passed down through the recursive calls rather than the values actually being copied. If an rvalue is passed (such as the constants in the previous examples), then the template parameters are deduced to be the plain type of the rvalue, and function arguments are bound by rvalue reference instead.
For the recursive calls, the parameters now refer to a named value (rest), and so are passed by lvalue reference. This is not a problem in this example, but if the actual operation being performed in place of the stream insertion depended on the lvalue/rvalue-ness of the parameters, then you could preserve this with std::forward:
print_comma_separated_list(std::forward<Rest>(rest)...);
This and the rvalue-reference parameter declaration show an additional feature of parameter packs: you can expand an expression for each item in the pack by placing the ellipsis at the end of the sub-expression that should be expanded. The std::forward example above is equivalent to:
print_comma_separated_list(std::forward<Rest1>(rest1),
std::forward<Rest2>(rest2),
std::forward<Rest3>(rest3),
...
std::forward<RestN>(restN));
Where the X suffix indicates the Xth element of the parameter pack. You can extend this to arbitrarily complex expressions. The key point is that any expression that uses a template parameter pack or a function argument pack must also use the ellipsis to expand that pack.
In summary, you can use pack expansions in template instantiations:
template<typename ... Args>
unsigned how_many_args(Args ... args)
{
return sizeof...(args);
}
In the current release of boost, you can specify up to 20 types in the list through the use of the boost preprocessor and metaprogramming libraries. With variadic templates, this could be unbounded, allowing you to declare a variable that can hold an instance of one of 100 types. It also simplifies the code as you no longer need to rely on preprocessor and metaprogramming tricks.
A similar case is std::tuple, except it holds an instance for each entry in the list rather than the either/or choice of boost::variant. You can write a simple tuple class quite easily. First, declare that simple_tuple is a variadic template without specifying any of the details.
template<typename ... Types>
class simple_tuple;
Then you specialize it for an empty list: if there aren't any types in the list, then you haven't got any values. So it's just an empty class.
template<>
class simple_tuple<>
{};
Now you can specialize for a list of at least one element by recursion. A list of N elements is a single element plus a list of N-1 elements.
template<typename First,typename ... Rest>
class simple_tuple<First,Rest...>:
private simple_tuple<Rest...>
{
First member;
public:
simple_tuple(First const& f,Rest const& ... rest):
simple_tuple<Rest...>(rest...),
member(f)
{}
First const& head() const
{
return member;
}
simple_tuple<Rest...> const& rest() const
{
return *this;
}
};
The simple accessor functions allow you to get at the data:
This works, but it's a bit unwieldy, which is why std::tuple has a helpful get() function to retrieve a numbered element. You can write a get_tuple_entry() function for your tuple too (see Listing 1).
In order to obtain the type and value of the N-th element of your simple tuple, you need to use a helper class (simple_tuple_entry) because you cannot partially specialize a function template. The get_tuple_entry function itself just passes everything on to simple_tuple_entry, either to retrieve the type of the entry or to retrieve the value itself.
The simple_tuple_entry class again has two specializations. The first is for the 0-th element, which is therefore first in the list and corresponds to the head() function for the tuple. If your index is not zero, you still need a list of at least one element. In this case, you discard the first element and find the (index-1)-th element of the rest() of your original tuple.
This get_tuple_entry function makes element accesses much easier: you can just say get_tuple_entry<5>(t) to get the sixth element of your tuple. The following simple code will thus output "42,a,3.141":
int main()
{
simple_tuple<int,char,double> st(42,'a',3.141);
std::cout<<get_tuple_entry<0>(st)<<","
<<get_tuple_entry<1>(st)<<","
<<get_tuple_entry<2>(st)<<std::endl;
}
Of course std::tuple has a lot more features and is correspondingly more complex, but the basis is still something similar to this approach.
| DevX is a division of Internet.com. © Copyright 2010 Internet.com. All Rights Reserved. Legal Notices |