Type-safe Variadic Functions
Variadic template parameters aren't restricted to class templates; you can use them with function templates too. In fact, one of the most powerful uses of variadic template parameters is with function templates, where they combine with automatic template parameter type deduction to provide type-safe variadic functions. For example, you could use a variadic function template to print a comma-separated list of arbitrary values:
void print_comma_separated_list(T value)
template<typename First,typename ... Rest>
void print_comma_separated_list(First first,Rest ... rest)
You can then call this with a list of values of any type that can be written to std::cout:
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:
"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.
The Body of the Function
The body of the function is actually relatively straightforward: you print out the first item in the list, followed by a comma, and then make a recursive call to print the rest of the list. The recursive call uses another pack expansion to pass the rest of the elements as individual arguments. Because the first argument had its own template parameter, it is not part of the variadic pack and it will not be included, which is exactly the behavior you're after. The sample call in the previous section will thus cause the following recursive calls:
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:
void print_comma_separated_list(T&& value)
template<typename First,typename ... Rest>
void print_comma_separated_list(First&& first,Rest&& ... 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:
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:
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:
- As part of the template argument list
- In function call expressions as part of the function argument list
- As part of a brace-enclosed initializer list
- In the base class list of a class definition or member initializer list for a constructor
How Many Elements Are There in a Parameter Pack?
There is one more special feature associated with variadic templates: the sizeof...
operator. Whereas the normal sizeof
operator gives you the size in bytes of a type or object, the sizeof...
operator tells you how many values or types there are in a pack. This is the only time that a pack can appear in an expression without an ellipsis for the pack expansion. For example, the following function returns the number of supplied arguments:
template<typename ... Args>
unsigned how_many_args(Args ... args)