Variadic templates

Probably the greatest difference between generic programming in C and C++ is type safety. It is possible to write generic code in C—the standard function qsort() is a perfect example—it can sort values of any type and they are passed in using a void* pointer, which can really be a pointer to any type. Of course, the programmer has to know what the real type is and cast the pointer to the right type. In a generic C++ program, the types are either explicitly specified or deduced at the time of the instantiation, and the type system for generic types is as strong as it is for regular types. Unless we want a function with an unknown number of arguments, that is, prior to C++11, the only way was the old C-style variadic functions where the compiler had no idea what the argument types were; the programmer just had to know and unpack the variable arguments correctly.

C++11 introduced the modern equivalent to the variadic function—the variadic template. We can now declare a generic function with any number of arguments:

template <typename ... T>
auto sum(const T& ... x);

This function takes one or more arguments, possibly of different types, and computes their sum. The return type is not easy to determine, but, fortunately, we can let the compiler figure it out—we just declare the return type as auto. How do we actually implement the function to add up the unknown number of values whose types we can't name, not even as generic types? In C++17, it's easy, because it has fold expressions:

template <typename ... T>
auto sum(const T& ... x) {
return (x + ...);
}
sum(5, 7, 3); // 15, int
sum(5, 7L, 3); // 15, long
sum(5, 7L, 2.9); // 14.9, double

In C++14, as well as in C++17, when a fold expression is not sufficient (and they are useful only in limited contexts, mostly when the arguments and combines using binary or unary operators), the standard technique is recursion, which is ever-popular in template programming:

template <typename T1>
auto sum(const T& x1) {
return x1;
}
template <typename T1, typename ... T>
auto sum(const T1& x1, const T& ... x) {
return x1 + sum(x ...);
}

The first overload (not a partial specialization!) is for the sum() function with one argument of any type. That value is returned. The second overload is for more than one argument, and the first argument is explicitly added to the sum of the remaining arguments. The recursion continues until there is only one argument left, at which point the other overload is called and the recursion stops. This is the standard technique for unraveling the parameter packs in variadic templates, and we will see this many times in this book. The compiler will inline all the recursive function calls and generate the straightforward code that adds all arguments together.

The class templates can also be variadic—they have an arbitrary number of type arguments and can build the classes from a varying number of objects of different types. The declaration is similar to that of a function template. For example, let's build a class template, Group, that can hold any number of objects of different types and return the right object when it's converted to one of the types it holds:

template <typename ... T>
struct Group;

The usual implementation of such templates is again recursive, using deeply nested inheritance, although a non-recursive implementation is sometimes possible. We will see one in the next section. The recursion has to be terminated when there is only one type parameter left. This is done using a partial specialization, so we will leave the general template we showed previously as a declaration only, and define the specialization for one type parameter:

template <typename T1>
struct Group<T1> {
T1 t1_;
Group() = default;
explicit Group(const T1& t1) : t1_(t1) {}
explicit Group(T1&& t1) : t1_(std::move(t1)) {}
explicit operator const T1&() const { return t1_; }
explicit operator T1&() { return t1_; }
};

This class holds the value of one type, T1, initializes it by copy or move, and returns a reference to it when converted to the T1 type. The specialization for an arbitrary number of type parameters contains the first one as a data member, together with the corresponding initialization and conversion methods, and inherits from the Group class template of the remaining types:

template <typename T1, typename ... T>
struct Group<T1, T ...> : Group<T ...> {
T1 t1_;
Group() = default;
explicit Group(const T1& t1, T&& ... t) :
Group<T ...>(std::forward<T>(t) ...), t1_(t1) {}
explicit Group(T1&& t1, T&& ... t) :
Group<T ...>(std::forward<T>(t) ...), t1_(std::move(t1)) {}
explicit operator const T1&() const { return t1_; }
explicit operator T1&() { return t1_; }
};

For every type contained in a Group class, there are two possible ways it can be initialized—copy or move. Fortunately, we do not have to spell out the constructors for every combination of copy and move operations. Instead, we have two versions of the constructor for the two ways to initialize the first argument (the one stored in the specialization); we use perfect forwarding for the remaining arguments.

Now, we can use our Group class template to hold some values of different types (it cannot handle multiple values of the same type, since the attempt to retrieve this type would be ambiguous):

Group<int, long> g(3, 5);
int(g); // 3
long(t); // 5

It is rather inconvenient to write all the group types explicitly and to make sure they match the argument types. The usual solution to this problem is to use a helper function template (a variadic template, of course) to take advantage of the template argument deduction:

template <typename ... T>
auto makeGroup(T&& ... t) {
return Group<T ...>(std::forward<T>(t) ...);
}
auto g = makeGroup(3, 2.2, std::string("xyz"));
int(g); // 3
double(g); // 2.2
std::string(g); // "xyz"

Note that the C++ standard library contains a class template, std::tuple, which is a much more complete and full-featured version of our Group.

The variadic templates, especially combined with perfect forwarding, are extremely useful for writing very general template classes—for example, a vector can contain objects of an arbitrary type, and, to construct these objects in place instead of copying them, we have to call constructors with a different number of arguments. When the vector template is written, there is no way to know how many arguments are needed to initialize the objects the vector will contain, so a variadic template has to be used (indeed, the in-place constructors of std::vector, such as emplace_back, are variadic templates).

There is one more kind of template-like entity in C++ that we have to mention, one that has the appearance of both a class and a function—the lambda expression. The next section is dedicated to this.

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

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