Chapter 4

Variadic Templates

Since C++11, templates can have parameters that accept a variable number of template arguments. This feature allows the use of templates in places where you have to pass an arbitrary number of arguments of arbitrary types. A typical application is to pass an arbitrary number of parameters of arbitrary type through a class or framework. Another application is to provide generic code to process any number of parameters of any type.

4.1 Variadic Templates

Template parameters can be defined to accept an unbounded number of template arguments. Templates with this ability are called variadic templates.

4.1.1 Variadic Templates by Example

For example, you can use the following code to call print() for a variable number of arguments of different types:

basics/varprint1.hpp

#include <iostream>
 
void print ()
{
}
 
template<typename T, typename… Types>
void print (T firstArg, Types… args)
{
  std::cout << firstArg << ’ ’;  //print first argument
  print(args…);                 // call print() for remaining arguments
}

If one or more arguments are passed, the function template is used, which by specifying the first argument separately allows printing of the first argument before recursively calling print() for the remaining arguments. These remaining arguments named args are a function parameter pack:

void print (T firstArg, Types… args)

using different “Types” specified by a template parameter pack:

template<typename T, typename… Types>

To end the recursion, the nontemplate overload of print() is provided, which is issued when the parameter pack is empty.

For example, a call such as

std::string s("world");
print (7.5, "hello", s);

would output the following:

7.5
hello
world

The reason is that the call first expands to

print<double, char const*, std::string> (7.5, "hello", s);

with

firstArg having the value 7.5 so that type T is a double and

args being a variadic template argument having the values "hello" of type char const* and "world" of type std::string.

After printing 7.5 as firstArg, it calls print() again for the remaining arguments, which then expands to:

print<char const*, std::string> ("hello", s);

with

firstArg having the value "hello" so that type T is a char const* here and

args being a variadic template argument having the value of type std::string.

After printing "hello" as firstArg, it calls print() again for the remaining arguments, which then expands to:

print<std::string> (s);

with

firstArg having the value "world" so that type T is a std::string now and

args being an empty variadic template argument having no value.

Thus, after printing "world" as firstArg, we calls print() with no arguments, which results in calling the nontemplate overload of print() doing nothing.

4.1.2 Overloading Variadic and Nonvariadic Templates

Note that you can also implement the example above as follows:

basics/varprint2.hpp

#include <iostream>
 
template<typename T>
void print (T arg)
{
  std::cout << arg << ’ ’;  //print passed argument
}
 
template<typename T, typename… Types>
void print (T firstArg, Types… args)
{
  print(firstArg);                // call print() for the first argument
  print(args…);                 // call print() for remaining arguments
}

That is, if two function templates only differ by a trailing parameter pack, the function template without the trailing parameter pack is preferred.1 Section C.3.1 on page 688 explains the more general overload resolution rule that applies here.

4.1.3 Operator sizeof…

C++11 also introduced a new form of the sizeof operator for variadic templates: sizeof…. It expands to the number of elements a parameter pack contains. Thus,

template<typename T, typename… Types>
void print (T firstArg, Types… args)
{
  std::cout << sizeof…(Types) << ’ ’;  //print number of remaining types
  std::cout << sizeof…(args) << ’ ’;   //print number of remaining args
  …
}

twice prints the number of remaining arguments after the first argument passed to print(). As you can see, you can call sizeof… for both template parameter packs and function parameter packs.

This might lead us to think we can skip the function for the end of the recursion by not calling it in case there are no more arguments:

template<typename T, typename… Types>
void print (T firstArg, Types… args)
{
  std::cout << firstArg << ’ ’;
  if (sizeof…(args) > 0) {      //error if sizeof…(args)==0
    print(args…);               // and no print() for no arguments declared
  }
}

However, this approach doesn’t work because in general both branches of all if statements in function templates are instantiated. Whether the instantiated code is useful is a run-time decision, while the instantiation of the call is a compile-time decision. For this reason, if you call the print() function template for one (last) argument, the statement with the call of print(args…) still is instantiated for no argument, and if there is no function print() for no arguments provided, this is an error.

However, note that since C++17, a compile-time if is available, which achieves what was expected here with a slightly different syntax. This will be discussed in Section 8.5 on page 134.

4.2 Fold Expressions

Since C++17, there is a feature to compute the result of using a binary operator over all the arguments of a parameter pack (with an optional initial value).

For example, the following function returns the sum of all passed arguments:

template<typename… T>
auto foldSum (T… s) {
  return (… + s);   // ((s1 + s2) + s3)
}

If the parameter pack is empty, the expression is usually ill-formed (with the exception that for operator && the value is true, for operator || the value is false, and for the comma operator the value for an empty parameter pack is void()).

Table 4.1 lists the possible fold expressions.

Fold Expression

Evaluation

( … op pack )

((( pack1 op pack2 ) op pack3 )op packN )

( pack op … )

( pack1 op (( packN-1 op packN )))

( init op op pack )

((( init op pack1 ) op pack2 )op packN )

( pack op op init )

( pack1 op (( packN op init )))

Table 4.1. Fold Expressions (since C++17)

You can use almost all binary operators for fold expressions (see Section 12.4.6 on page 208 for details). For example, you can use a fold expression to traverse a path in a binary tree using operator ->*:

basics/foldtraverse.cpp

// define binary tree structure and traverse helpers:
struct Node {
  int value;
  Node* left;
  Node* right;
  Node(int i=0) : value(i), left(nullptr), right(nullptr) {
  }
  …
};
auto left = &Node::left;
auto right = &Node::right;
 
// traverse tree, using fold expression:
template<typename T, typename… TP>
Node* traverse (T np, TP… paths) {
  return (np ->* … ->* paths); // np ->* paths1 ->* paths2
}
 
int main()
{
  // init binary tree structure:
  Node* root = new Node{0};
  root->left = new Node{1};
  root->left->right = new Node{2};
  …
  // traverse binary tree:
  Node* node = traverse(root, left, right);
  …
}

Here,

(np ->* … ->* paths)

uses a fold expression to traverse the variadic elements of paths from np.

With such a fold expression using an initializer, we might think about simplifying the variadic template to print all arguments, introduced above:

template<typename… Types>
void print (Types const&… args)
{
  (std::cout << … << args) << ’ ’;
}

However, note that in this case no whitespace separates all the elements from the parameter pack. To do that, you need an additional class template, which ensures that any output of any argument is extended by a space:

basics/addspace.hpp

template<typename T>
class AddSpace
{
  private:
    T const& ref;                  // refer to argument passed in constructor
  public:
    AddSpace(T const& r): ref(r) {
    }
    friend std::ostream& operator<< (std::ostream& os, AddSpace<T> s) {
      return os << s.ref <<’’;   // output passed argument and a space
    }
};
 
template<typename… Args>
void print (Args… args) {
  ( std::cout << … << AddSpace(args) ) << ’ ’;
}


Note that the expression AddSpace(args) uses class template argument deduction (see Section 2.9 on page 40) to have the effect of AddSpace<Args>(args), which for each argument creates an AddSpace object that refers to the passed argument and adds a space when it is used in output expressions.

See Section 12.4.6 on page 207 for details about fold expressions.

4.3 Application of Variadic Templates

Variadic templates play an important role when implementing generic libraries, such as the C++ standard library.

One typical application is the forwarding of a variadic number of arguments of arbitrary type. For example, we use this feature when:

• Passing arguments to the constructor of a new heap object owned by a shared pointer:

// create shared pointer to complex<float> initialized by 4.2 and 7.7:
auto sp = std::make_shared<std::complex<float>>(4.2, 7.7);

• Passing arguments to a thread, which is started by the library:

std::thread t (foo, 42, "hello");        //call foo(42,"hello") in a separate thread

• Passing arguments to the constructor of a new element pushed into a vector:

std::vector<Customer> v;

v.emplace("Tim", "Jovi", 1962);        //insert a Customer initialized by three arguments

Usually, the arguments are “perfectly forwarded” with move semantics (see Section 6.1 on page 91), so that the corresponding declarations are, for example:

namespace std {
  template<typename T, typename… Args> shared_ptr<T>
  make_shared(Args&&… args);
 
  class thread {
   public:
    template<typename F, typename… Args>
    explicit thread(F&& f, Args&&… args);    
    …
  };
 
  template<typename T, typename Allocator = allocator<T>>
  class vector {
   public:
    template<typename… Args> reference emplace_back(Args&&… args);
    …
  };
}

Note also that the same rules apply to variadic function template parameters as for ordinary parameters. For example, if passed by value, arguments are copied and decay (e.g., arrays become pointers), while if passed by reference, parameters refer to the original parameter and don’t decay:

// args are copies with decayed types:
template<typename… Args> void foo (Args… args);
// args are nondecayed references to passed objects:
template<typename… Args> void bar (Args const&… args);

4.4 Variadic Class Templates and Variadic Expressions

Besides the examples above, parameter packs can appear in additional places, including, for example, expressions, class templates, using declarations, and even deduction guides. Section 12.4.2 on page 202 has a complete list.

4.4.1 Variadic Expressions

You can do more than just forward all the parameters. You can compute with them, which means to compute with all the parameters in a parameter pack.

For example, the following function doubles each parameter of the parameter pack args and passes each doubled argument to print():

template<typename… T>
void printDoubled (T const&… args)
{
  print (args + args…);
}

If, for example, you call

printDoubled(7.5, std::string("hello"), std::complex<float>(4,2));

the function has the following effect (except for any constructor side effects):

print(7.5 + 7.5,
      std::string("hello") + std::string("hello"),
      std::complex<float>(4,2) + std::complex<float>(4,2);

If you just want to add 1 to each argument, note that the dots from the ellipsis may not directly follow a numeric literal:

template<typename… T>
void addOne (T const&… args)
{
  print (args + 1…);    // ERROR: 1… is a literal with too many decimal points
  print (args + 1 …);   // OK
  print ((args + 1)…);  // OK
}

Compile-time expressions can include template parameter packs in the same way. For example, the following function template returns whether the types of all the arguments are the same:

template<typename T1, typename… TN>
constexpr bool isHomogeneous (T1, TN…)
{
  return (std::is_same<T1,TN>::value && …);  // since C++17
}

This is an application of fold expressions (see Section 4.2 on page 58): For

isHomogeneous(43, -1, "hello")

the expression for the return value expands to

std::is_same<int,int>::value && std::is_same<int,char const*>::value

and yields false, while

isHomogeneous("hello", "", "world", "!")

yields true because all passed arguments are deduced to be char const* (note that the argument types decay because the call arguments are passed by value).

4.4.2 Variadic Indices

As another example, the following function uses a variadic list of indices to access the corresponding element of the passed first argument:

template<typename C, typename… Idx>
void printElems (C const& coll, Idx… idx)
{
  print (coll[idx]…);
}

That is, when calling

std::vector<std::string> coll = {"good", "times", "say", "bye"}; printElems(coll,2,0,3);

the effect is to call

print (coll[2], coll[0], coll[3]);

You can also declare nontype template parameters to be parameter packs. For example:

template<std::size_t… Idx, typename C>
void printIdx (C const& coll)
{
  print(coll[Idx]…);
}

allows you to call

std::vector<std::string> coll = {"good", "times", "say", "bye"}; printIdx<2,0,3>(coll);

which has the same effect as the previous example.

4.4.3 Variadic Class Templates

Variadic templates can also be class templates. An important example is a class where an arbitrary number of template parameters specify the types of corresponding members:

template<typename… Elements>
class Tuple;
 
Tuple<int, std::string, char> t;    // t can hold integer, string, and character

This will be discussed in Chapter 25.

Another example is to be able to specify the possible types objects can have:

template<typename… Types>
class Variant;

Variant<int, std::string, char> v;        // v can hold integer, string, or character

This will be discussed in Chapter 26.

You can also define a class that as a type represents a list of indices:

// type for arbitrary number of indices:
template<std::size_t…>
struct Indices {
};

This can be used to define a function that calls print() for the elements of a std::array or std::tuple using the compile-time access with get<>() for the given indices:

template<typename T, std::size_t… Idx>
void printByIdx(T t, Indices<Idx…>)
{
  print(std::get<Idx>(t)…);
}

This template can be used as follows:

std::array<std::string, 5> arr = {"Hello", "my", "new", "!", "World"}; printByIdx(arr, Indices<0, 4, 3>());

or as follows:

    auto t = std::make_tuple(12, "monkeys", 2.0);
printByIdx(t, Indices<0, 1, 2>());

This is a first step towards meta-programming, which will be discussed in Section 8.1 on page 123 and Chapter 23.

4.4.4 Variadic Deduction Guides

Even deduction guides (see Section 2.9 on page 42) can be variadic. For example, the C++ standard library defines the following deduction guide for std::arrays:

namespace std {
  template<typename T, typename… U> array(T, U…)
    -> array<enable_if_t<(is_same_v<T, U> && …), T>,
             (1 + sizeof…(U))>;
}

An initialization such as

std::array a{42,45,77};

deduces T in the guide to the type of the element, and the various U… types to the types of the subsequent elements. The total number of elements is therefore 1 + sizeof…(U):

std::array<int, 3> a{42,45,77};

The std::enable_if<> expression for the first array parameter is a fold expression that (as introduced as isHomogeneous() in Section 4.4.1 on page 62) expands to:

is_same_v<T, U1> && is_same_v<T, U2> && is_same_v<T, U3> …

If the result is not true (i.e., not all the element types are the same), the deduction guide is discarded and the overall deduction fails. This way, the standard library ensures that all elements must have the same type for the deduction guide to succeed.

4.4.5 Variadic Base Classes and using

Finally, consider the following example:

basics/varusing.cpp

#include <string>
#include <unordered_set>
 
class Customer
{
  private:
    std::string name;
  public:
    Customer(std::string const& n) : name(n) { }
    std::string getName() const { return name; }
};
 
struct CustomerEq {
    bool operator() (Customer const& c1, Customer const& c2) const {
      return c1.getName() == c2.getName();
    }
};
 
struct CustomerHash {
    std::size_t operator() (Customer const& c) const {
      return std::hash<std::string>()(c.getName());
    }
};
 
// define class that combines operator() for variadic base classes:
template<typename… Bases>
struct Overloader : Bases…
{
      using Bases::operator()…;  // OK since C++17
};
 
int main()
{
  // combine hasher and equality for customers in one type:
  using CustomerOP = Overloader<CustomerHash,CustomerEq>;
 
  std::unordered_set<Customer,CustomerHash,CustomerEq> coll1;
  std::unordered_set<Customer,CustomerOP,CustomerOP> coll2;
  …
}

Here, we first define a class Customer and independent function objects to hash and compare Customer objects. With

template<typename… Bases>
struct Overloader : Bases…
{
    using Bases::operator()…;  // OK since C++17
};

we can define a class derived from a variadic number of base classes that brings in the operator() declarations from each of those base classes. With

using CustomerOP = Overloader<CustomerHash,CustomerEq>;

we use this feature to derive CustomerOP from CustomerHash and CustomerEq and enable both implementations of operator() in the derived class.

See Section 26.4 on page 611 for another application of this technique.

4.5 Summary

• By using parameter packs, templates can be defined for an arbitrary number of template parameters of arbitrary type.

• To process the parameters, you need recursion and/or a matching nonvariadic function.

• Operator sizeof… yields the number of arguments provided for a parameter pack.

• A typical application of variadic templates is forwarding an arbitrary number of arguments of arbitrary type.

• By using fold expressions, you can apply operators to all arguments of a parameter pack.

1 Initially, in C++11 and C++14 this was an ambiguity, which was fixed later (see [CoreIssue1395]), but all compilers handle it this way in all versions.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.87.156