Chapter 14 - Notes - Variadic Templates

Variadic Templates

Variadic templates are the only way of implementing variadic functions in C++. Variadic functions are simply functions that accept an unspecified number of elements.

Motivation

Consider the following problem statement: if we have a sum function that sums two integers:

int sum(int a, int b);

We may later also want a sum function that sums three integers, and four integers, so on so forth. The only way we know how to this to our current knowledge is with function overloading:

int sum(int a, int b, int c);  // Three elements
int sum(int a, int b, int c, int d);  // Four elements 

However this way of doing things is quite terse, and requires you to maintain a bunch of functions that do the same thing.

This is where variadic templates come in handy.

Syntax

The syntax of a variadic template is most confusing the first time you see it. Consider the following simple example:

template <typename... Ts>
void ignore(Ts... ts) {}

The variadic function ignore at the moment does nothing (empty function body). Let's dissect its syntax:

template <typename... Ts>

  • typename... Ts declares Ts as a template parameter pack.

  • You will often see the naming conventions for these in the form of plurals, e.g. Ts, meaning multiple Ts. Other common names are Args or Rest.

  • The key part is the ellipsis operator (...) denoting that Ts is indeed a template parameter pack.

void ignore(Ts... ts)

  • This is the function signature, which accepts a function parameter pack (Ts... ts), i.e. a bag of parameters.

  • The type of the elements in the function parameter pack are given by the template parameter pack.

  • The key part is the ellipsis operator (...) denoting that ts refers to the template parameter pack.

It may be useful to be able to mentally unwrap the above definition as:

template <typename T1, typename T2, ..., typename Tn>
void ignore(T1 t1, T2 t2, ... Tn tn) {};

Following this, it should be clear that each value in the template parameter pack can be a different type:

ignore(1, 2.0, true);

Has the same effect as:

ignore<int, double, bool>(1, 2.0, true);

Usage

Let's attempt to implement a variadic sum function, as we so desired. You may expect the function to look like the following:

template <typename... Ts>
double sum(Ts... ts) {
    double result = 0.0;
    for (auto el : ts)
        result += el;
    return result;
}

Unfortunately when it comes to variadic functions, you can't approach the problem in a standard iterative C++ style. You will need to write the variadic functions themselves recursively - with a base case and a recursive case:

// The base case: we just have a single number.
template <typename T>
double sum(T t) {
  return t;
}

// The recursive case: we take a number, alongside
// some other numbers, and produce their sum.
template <typename T, typename... Rest>
double sum(T t, Rest... rest) {
  return t + sum(rest...);
}

The base case accepts one argument T.

The recursive case accepts one or more arguments, T and Rest (recall that a template parameter pack can be empty).

Mechanism

How exactly does this work? Let’s trace what happens when we try to call, for example, sum(1.0, 2.0, 3.0). This is going to be a bit repetitive, but it’s worth it to walk through the process at least once.

  1. The compiler generates code for sum(1.0, 2.0, 3.0). There are two competing overloads for sum here: the base case, and the recursive case. Since we’re passing in three arguments, the base case does not apply (it only accepts a single argument), so we select the recursive case.

  2. Type deduction is performed – the compiler deduces T = double, and puts the rest in our parameter pack, with Rest = <double, double>.

  3. The compiler generates code for t + sum(rest...). It sees the recursive call to sum(rest...). Note the use of ... to ‘unpack’ the template argument – this has the effect of transforming sum(rest...) to sum(2.0, 3.0).

  4. The compiler generates code for sum(2.0, 3.0). As in the first case, there are two competing overloads: the base case, and the recursive case. The base case once again does not apply as we have more than one argument, so we select the recursive case.

  5. Type deduction is performed – the compiler deduces T = double, and Rest = <double>. It’s subtle, but notice that we have now unpacked our original <double, double> pack to T = double and Rest = <double>.

  6. The compiler generates code for t + sum(rest...). It sees the recursive call to sum(rest...) – this time, with sum(rest...) expanding to simply sum(3.0).

  7. The compiler generates code for sum(3.0). We have now finally hit our base case: the overload taking only T is more specialized, relative to the overload taking both T and Ts.... The compiler generates code for the base case, and we’re done with the recursion.

Let’s outline the main techniques we’ve learned here:

  • To unpack a parameter pack, use a templated function taking one (or more) parameters explicitly, and the ‘rest’ of the parameters as a template parameter pack.

  • Recurse towards a base case: call your recursive function with the ‘rest…’ arguments, and let the compiler unpack your parameters in subsequent calls to your recursive function.

  • Allow your base case to overload your recursive case – it will be selected in preference to the recursive case as soon as the parameter pack is empty.

Conclusion

We’ve outlined some of the basic tools and patterns for implementing variadic functions:

  • Use recursion to implement variadic functions – implement a base case, and a recursive case, and have the recursive case reduce to a base case call;

  • Use ... to unpack parameter packs, or in more clever contexts, to unpack whole expressions containing a parameter pack.

Last updated

Was this helpful?