Team LiB
Previous Section Next Section

16.4. Variadic Templates

 
Image
Image

A variadic template is a template function or class that can take a varying number of parameters. The varying parameters are known as a parameter pack. There are two kinds of parameter packs: A template parameter pack represents zero or more template parameters, and a function parameter pack represents zero or more function parameters.

 

We use an ellipsis to indicate that a template or function parameter represents a pack. In a template parameter list, class... or typename... indicates that the following parameter represents a list of zero or more types; the name of a type followed by an ellipsis represents a list of zero or more nontype parameters of the given type. In the function parameter list, a parameter whose type is a template parameter pack is a function parameter pack. For example:

 

 

// Args is a template parameter pack; rest is a function parameter pack
// Args represents zero or more template type parameters
// rest represents zero or more function parameters
template <typename T, typename... Args>
void foo(const T &t, const Args& ... rest);

 

declares that foo is a variadic function that has one type parameter named T and a template parameter pack named Args. That pack represents zero or more additional type parameters. The function parameter list of foo has one parameter, whose type is a const & to whatever type T has, and a function parameter pack named rest. That pack represents zero or more function parameters.

 

As usual, the compiler deduces the template parameter types from the function’s arguments. For a variadic template, the compiler also deduces the number of parameters in the pack. For example, given these calls:

 

 

int i = 0; double d = 3.14; string s = "how now brown cow";
foo(i, s, 42, d);    // three parameters in the pack
foo(s, 42, "hi");    // two parameters in the pack
foo(d, s);           // one parameter in the pack
foo("hi");           // empty pack

 

the compiler will instantiate four different instances of foo:

 

 

void foo(const int&, const string&, const int&, const double&);
void foo(const string&, const int&, const char[3]&);
void foo(const double&, const string&);
void foo(const char[3]&);

 

In each case, the type of T is deduced from the type of the first argument. The remaining arguments (if any) provide the number of, and types for, the additional arguments to the function.

 

The sizeof... Operator

 
Image

When we need to know how many elements there are in a pack, we can use the sizeof... operator. Like sizeof4.9, p. 156), sizeof... returns a constant expression (§ 2.4.4, p. 65) and does not evaluate its argument:

 

 

template<typename ... Args> void g(Args ... args) {
    cout << sizeof...(Args) << endl;  // number of type parameters
    cout << sizeof...(args) << endl;  // number of function parameters
}

 

Exercises Section 16.4

 

Exercise 16.51: Determine what sizeof...(Args) and sizeof...(rest) return for each call to foo in this section.

Exercise 16.52: Write a program to check your answer to the previous question.


 

16.4.1. Writing a Variadic Function Template

 
Image

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:

 
Image
 

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).

 

Image Warning

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.

 

 

Exercises Section 16.4.1

 

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.


 

16.4.2. Pack Expansion

 
Image

Aside from taking its size, the only other thing we can do with a parameter pack is to expand it. When we expand a pack, we also provide a pattern to be used on each expanded element. Expanding a pack separates the pack into its constituent elements, applying the pattern to each element as it does so. We trigger an expansion by putting an ellipsis (. . . ) to the right of the pattern.

 

For example, our print function contains two expansions:

 

 

template <typename T, typename... Args>
ostream &
print(ostream &os, const T &t, const Args&... rest)// expand Args
{
    os << t << ", ";
    return print(os, rest...);                     // expand rest
}

 

The first expansion expands the template parameter pack and generates the function parameter list for print. The second expansion appears in the call to print. That pattern generates the argument list for the call to print.

 

The expansion of Args applies the pattern const Args& to each element in the template parameter pack Args. The expansion of this pattern is a comma-separated list of zero or more parameter types, each of which will have the form const type&. For example:

 

 

print(cout, i, s, 42);  // two parameters in the pack

 

The types of the last two arguments along with the pattern determine the types of the trailing parameters. This call is instantiated as

 

 

ostream&
print(ostream&, const int&, const string&, const int&);

 

The second expansion happens in the (recursive) call to print. In this case, the pattern is the name of the function parameter pack (i.e., rest). This pattern expands to a comma-separated list of the elements in the pack. Thus, this call is equivalent to

 

print(os, s, 42);

 
Understanding Pack Expansions
 

The expansion of the function parameter pack in print just expanded the pack into its constituent parts. More complicated patterns are also possible when we expand a function parameter pack. For example, we might write a second variadic function that calls debug_rep16.3, p. 695) on each of its arguments and then calls print to print the resulting strings:

 

 

// call debug_rep on each argument in the call to print
template <typename... Args>
ostream &errorMsg(ostream &os, const Args&... rest)
{
    // print(os, debug_rep(a1), debug_rep(a2), ..., debug_rep(an)
    return print(os, debug_rep(rest)...);
}

 

The call to print uses the pattern debug_rep(rest). That pattern says that we want to call debug_rep on each element in the function parameter pack rest. The resulting expanded pack will be a comma-separated list of calls to debug_rep. That is, a call such as

 

 

errorMsg(cerr, fcnName, code.num(), otherData, "other", item);

 

will execute as if we had written

 

 

print(cerr, debug_rep(fcnName), debug_rep(code.num()),
            debug_rep(otherData), debug_rep("otherData"),
            debug_rep(item));

 

In contrast, the following pattern would fail to compile:

 

 

// passes the pack to debug_rep; print(os, debug_rep(a1, a2, ..., an))
print(os, debug_rep(rest...)); // error: no matching function to call

 

The problem here is that we expanded rest in the call to debug_rep. This call would execute as if we had written

 

 

print(cerr, debug_rep(fcnName, code.num(),
                      otherData, "otherData", item));

 

In this expansion, we attempted to call debug_rep with a list of five arguments. There is no version of debug_rep that matches this call. The debug_rep function is not variadic and there is no version of debug_rep that has five parameters.

 

Image Note

The pattern in an expansion applies separately to each element in the pack.

 

 

Exercises Section 16.4.2

 

Exercise 16.56: Write and test a variadic version of errorMsg.

Exercise 16.57: Compare your variadic version of errorMsg to the error_msg function in § 6.2.6 (p. 220). What are the advantages and disadvantages of each approach?

 

 

16.4.3. Forwarding Parameter Packs

 
Image

Under the new standard, we can use variadic templates together with forward to write functions that pass their arguments unchanged to some other function. To illustrate such functions, we’ll add an emplace_back member to our StrVec class (§ 13.5, p. 526). The emplace_back member of the library containers is a variadic member template (§ 16.1.4, p. 673) that uses its arguments to construct an element directly in space managed by the container.

 
Image

Our version of emplace_back for StrVec will also have to be variadic, because string has a number of constructors that differ in terms of their parameters. Because we’d like to be able to use the string move constructor, we’ll also need to preserve all the type information about the arguments passed to emplace_back.

 

As we’ve seen, preserving type information is a two-step process. First, to preserve type information in the arguments, we must define emplace_back’s function parameters as rvalue references to a template type parameter (§ 16.2.7, p. 693):

 

 

class StrVec {
public:
    template <class... Args> void emplace_back(Args&&...);
    // remaining members as in § 13.5 (p. 526)
};

 

The pattern in the expansion of the template parameter pack, &&, means that each function parameter will be an rvalue reference to its corresponding argument.

 

Second, we must use forward to preserve the arguments’ original types when emplace_back passes those arguments to construct16.2.7, p. 694):

 

 

template <class... Args>
inline
void StrVec::emplace_back(Args&&... args)
{
    chk_n_alloc(); // reallocates the StrVec if necessary
    alloc.construct(first_free++, std::forward<Args>(args)...);
}

 

The body of emplace_back calls chk_n_alloc13.5, p. 526) to ensure that there is enough room for an element and calls construct to create an element in the first_free spot. The expansion in the call to construct:

 

std::forward<Args>(args)...

 

expands both the template parameter pack, Args, and the function parameter pack, args. This pattern generates elements with the form

 

std::forward<Ti>(ti)

 

where Ti represents the type of the ith element in the template parameter pack and ti represents the ith element in the function parameter pack. For example, assuming svec is a StrVec, if we call

 

 

svec.emplace_back(10, 'c'); // adds cccccccccc as a new last element

 

the pattern in the call to construct will expand to

 

 

std::forward<int>(10), std::forward<char>(c)

 

By using forward in this call, we guarantee that if emplace_back is called with an rvalue, then construct will also get an rvalue. For example, in this call:

 

 

svec.emplace_back(s1 + s2); // uses the move constructor

 

the argument to emplace_back is an rvalue, which is passed to construct as

 

 

std::forward<string>(string("the end"))

 

The result type from forward<string> is string&&, so construct will be called with an rvalue reference. The construct function will, in turn, forward this argument to the string move constructor to build this element.

 

Advice: Forwarding and Variadic Templates

Variadic functions often forward their parameters to other functions. Such functions typically have a form similar to our emplace_back function:

 

 

// fun has zero or more parameters each of which is
// an rvalue reference to a template parameter type
template<typename... Args>
void fun(Args&&... args) // expands Args as a list of rvalue references
{
    // the argument to work expands both Args and args
    work(std::forward<Args>(args)...);
}

 

Here we want to forward all of fun’s arguments to another function named work that presumably does the real work of the function. Like our call to construct inside emplace_back, the expansion in the call to work expands both the template parameter pack and the function parameter pack.

 

Because the parameters to fun are rvalue references, we can pass arguments of any type to fun; because we use std::forward to pass those arguments, all type information about those arguments will be preserved in the call to work.

 

 

Exercises Section 16.4.3

 

Exercise 16.58: Write the emplace_back function for your StrVec class and for the Vec class that you wrote for the exercises in § 16.1.2 (p. 668).

 

Exercise 16.59: Assuming s is a string, explain svec.emplace_back(s).

Exercise 16.60: Explain how make_shared12.1.1, p. 451) works.

 

Exercise 16.61: Define your own version of make_shared.


 
Team LiB
Previous Section Next Section