In § 6.2.6 (p. 220) we saw that we can use an initializer_list
to define a function that can take a varying number of arguments. However, the arguments must have the same type (or types that are convertible to a common type). Variadic functions are used when we know neither the number nor the types of the arguments we want to process. As an example, we’ll define a function like our earlier error_msg
function, only this time we’ll allow the argument types to vary as well. We’ll start by defining a variadic function named print
that will print the contents of a given list of arguments on a given stream.
Variadic functions are often recursive (§ 6.3.2, p. 227). The first call processes the first argument in the pack and calls itself on the remaining arguments. Our print
function will execute this way—each call will print its second argument on the stream denoted by its first argument. To stop the recursion, we’ll also need to define a nonvariadic print
function that will take a stream and an object:
// function to end the recursion and print the last element
// this function must be declared before the variadic version of print is defined
template<typename T>
ostream &print(ostream &os, const T &t)
{
return os << t; // no separator after the last element in the pack
}
// this version of print will be called for all but the last element in the pack
template <typename T, typename... Args>
ostream &print(ostream &os, const T &t, const Args&... rest)
{
os << t << ", "; // print the first argument
return print(os, rest...); // recursive call; print the other arguments
}
The first version of print
stops the recursion and prints the last argument in the initial call to print
. The second, variadic, version prints the argument bound to t
and calls itself to print the remaining values in the function parameter pack.
The key part is the call to print
inside the variadic function:
return print(os, rest...); // recursive call; print the other arguments
The variadic version of our print
function takes three parameters: an ostream&
, a const T&
, and a parameter pack. Yet this call passes only two arguments. What happens is that the first argument in rest
gets bound to t
. The remaining arguments in rest
form the parameter pack for the next call to print
. Thus, on each call, the first argument in the pack is removed from the pack and becomes the argument bound to t
. That is, given:
print(cout, i, s, 42); // two parameters in the pack
the recursion will execute as follows:
The first two calls can match only the variadic version of print
because the nonvariadic version isn’t viable. These calls pass four and three arguments, respectively, and the nonvariadic print
takes only two arguments.
For the last call in the recursion, print(cout, 42)
, both versions of print
are viable. This call passes exactly two arguments, and the type of the first argument is ostream&
. Thus, the nonvariadic version of print
is viable.
The variadic version is also viable. Unlike an ordinary argument, a parameter pack can be empty. Hence, the variadic version of print
can be instantiated with only two parameters: one for the ostream&
parameter and the other for the const T&
parameter.
Both functions provide an equally good match for the call. However, a nonvariadic template is more specialized than a variadic template, so the nonvariadic version is chosen for this call (§ 16.3, p. 695).
A declaration for the nonvariadic version of print
must be in scope when the variadic version is defined. Otherwise, the variadic function will recurse indefinitely.
Exercise 16.53: Write your own version of the print
functions and test them by printing one, two, and five arguments, each of which should have different types.
Exercise 16.54: What happens if we call print
on a type that doesn’t have an <<
operator?
Exercise 16.55: Explain how the variadic version of print
would execute if we declared the nonvariadic version of print
after the definition of the variadic version.