C++ Variadic Templates
Aug 1, 2016
8 minutes read

Variadic templates allows us to have an arbitrary number of types and values for functions and classes1 in a type safe way. And since they are templates they are resolved at compile time.

The prototypical examples are a type safe version of printf and the STL std::tuple type, which is a heterogeneous list of types and corresponding values. For example:

auto p = std::make_tuple(3, "foo", 2.25, std::string("bar"));
std::cout << std::get<1>(p) << "\n";

// Outputs:
foo

p looks like this:

+---------+----------------+--------+-------------+
|   int   | const char[4]  | double | std::string |
+---------+----------------+--------+-------------+
|    3    |     "foo"      |  2.25  |    "bar"    |
+---------+----------------+--------+-------------+

(Note the const char[4] for foo, that’s for the trailing null character)

The C++ Standard doesn’t specify how a tuple is laid out in memory.

There is a lot of examples online how std::tuple and a type safe printf can be implemented, for instance see Bjarne Stroustrup’s C++ FAQ.

Parameter Packs

Consider the following variadic template function definition:

template <typename... Ts>     // (1)
void func(Ts... args)         // (2)
{
    // ...
}

The ellipsis (...) denotes a parameter pack. We get different parameter packs depending on where the ellipsis occur. For (1) we have a template parameter pack and for (2) a function parameter pack. A parameter pack can simply be thought of as a list of types or values.

For [0, N] parameters this can be thought of as:

template <typename T1, typename T2, typename N>
void func(T1 a1, T2 a2, N an)
{
}

Because it’s a compile time construct you cannot iterate over the parameter packs at runtime. Trying to do so is a compiler error:

template <typename... Ts>
void iterate(Ts... args)
{
    for (auto p : args) {
        // ...
    }
}

error: expression contains unexpanded parameter pack 'args'
    for (auto p : args) {
                  ^~~~

However, using pack expansion we can still accomplish this as we’ll see later.

Pack Expansion

In conjunction with parameter packs there’s also the notion of parameter pack expansion, or pack expansion for short. This is also denoted by an ellipsis; when it occurs to the right of a name it’s a pack expansion and when it occurs on the left of a name it’s a parameter pack. Consider this incomplete example:

template <typename T, typename... Ts>  // typename... = template parameter pack
T sum(T head, Ts... tail>              // Ts...       = function parameter pack
{
    return head + sum(tail...);        // tail...     = pack expansion
}

An ellipsis simply means “zero or more types/values”.

Pack expansion works in the following instances:

  • Template argument list
  • Function argument list
  • Initializer list
  • Base class specifier list
  • Member initializer list
  • sizeof... expressions
  • Lambda capture lists

Variadic Template Functions

Given the pack expansion example above it’s clear that variadic templates are generally implemented in terms of recursion2. As with all recursion we need the base case and the recursive case. For functions we accomplish that using function overloading.

Let’s look at the above incomplete example. As the name suggests we simply want to add a variable amount of values together. This can be implemented as:

// Base case.
template <typename T>
constexpr T sum(T v)
{
    return v;
}

// Recursion.
template <typename T, typename... Ts>
constexpr T sum(T head, Ts... tail)
{
    return head + sum(tail...);
}

int main()
{
    constexpr auto s = sum(1, 2, 3);
    std::cout << s << "\n";

    return 0;
}

Outputs:

6

It’s worth mentioning that we’re not seeing recursion as in traditional recursive functions. Instead, the compiler actually instantiates a separate function for each recursive step. We can inspect that with clang’s -ast-dump compiler option:

% clang -std=c++14 -Xclang -ast-dump sum.cpp

...
FunctionDecl 0x103880910 <line:12:1, line:15:1> line:12:13
   used constexpr sum 'int (int, int, int)'

FunctionDecl 0x103885fa0 <line:12:1, line:15:1> line:12:13
   used constexpr sum 'int (int, int)'

FunctionDecl 0x103886310 <line:12:1, line:15:1> line:12:13
   constexpr sum 'int (int)'
...

Let’s have a look at the pack expansion:

return head + sum(tail...);

tail is a list of values, and tail… will expand that list as:

+---+---+---+-----+---+
| 1 | 2 | 3 | ... | n |
+---+---+---+-----+---+
  | |                 |
  | ...................
  +----+      |
       |      |
       v      v   
sum(T head, T... tail) 

// Example ([] denotes a list):
sum(1, [2, 3, 4, 5])
sum(2, [3, 4, 5])
sum(3, [4, 5])
sum(4, [5])
sum(5)

So the first element in the parameter pack is passed as argument to the first function parameter, and the rest of the parameter pack is passed as argument to the second function parameter. When the parameter pack is empty we get a call to our base case function.

Variadic Template Classes

Variadic templates are very useful when working with classes. Like std::tuple shows they allow us to create heterogeneous containers of an arbitrary number of types and values. But that’s not all.

A particularly useful case for variadic template classes come when working with template template parameters. Consider the case where we want to give the option for clients to parameterize the underlying container type for our object. By default we use std::vector but some clients may want to use a std::set or similar.

Normally we solve this a template template parameter like so:

template <typename T,
    template <typename, typename = std::allocator<T>> class C = std::vector>
class Buffer {
    // ...

private:
    C<T> container;
};

This works great for the default case, and say when using a std::deque:

Buffer<int> bvi;                // Buffer uses std::vector<int>
Buffer<int, std::deque> bdi;    // Buffer uses std::deque<int>

Because they are parameterized with the same number of parameters. But let’s have a look what happens when a client wants to use a std::set:

Buffer<int, std::set> bsi;

error: template template argument has different template
    parameters than its corresponding template template parameter
    Buffer<int, std::set> bsi;

That’s no fun! Of course the problem here is that std::set is parameterized with a different number of parameters. We could provide a specialization for std::set but we quickly this is not a scalable solution. This is where variadic templates proves very useful, we can simply parameterize our type for an arbitrary number of parameters. We do that as follows:

template <typename T,
    template <typename...> class C = std::vector>
class Buffer {
    // ...

private:
    C<T> container;
};

// This now works:

Buffer<int, std::set> bsi;

Much better! This also have the upside of being less code to type and being easier to read. Readable code means maintainable code.

We can also use variadic template to derive from an arbitrary number of base classes. For example:

class Foo { // ... };
class Bar { // ... };
class Baz { // ... };

template <typename... Ts>
class Derived : public Ts... {
    // ...
};

Derived<Foo, Bar> p;
Derived<Foo, Bar, Baz> q;

Which is very convenient when implementing policy based class designs3. This is the technique behind the std::tuple implementation.

sizeof…

sizeof... simply returns the number of elements there are in a parameter pack:

template <typename... Ts>
constexpr size_t elements(Ts...)
{
    return sizeof...(Ts);
}

constexpr auto e = elements(1, 2, 3, 4, 5);

// e == 5

Initializer Lists

Remember how we couldn’t iterate over an parameter pack at run time? We can actually do that with the help of initializer lists since pack expansion works with them. For instance:

template <typename... Ts>
void iterate(Ts... args)
{
    // Expand args in initializer list.
    for (const auto& p : { args... })
        std::cout << p << "\n";
}

int main()
{
    iterate(1, 2, 3, 4, 5);
    return 0;
}

// Outputs:
1
2
3
4
5

Of course, this means we can use pack expansion in other places where we use initializer lists. For instance with arrays:

template <typename... Ts>
void init(Ts... args)
{
    const int p[] = { args... };
}

init(1, 2, 3);

Lambda Capture Lists

Pack expansion in lambda capture lists are pretty straightforward:

template <typename... Ts>
void wrapper(Ts... args)
{
    auto p = [&args...] {
        for (const auto& q : { args...})
            std::cout << q << "\n";
    });

    p();
}

wrapper(1, 2, 3);

// Output:
1
2
3

Forwarding

As mentioned in C++ Move Semantics we use forwarding to preserve the value category of our arguments as we pass them onto other functions. We can use pack expansions in those situations as well. For example:

template <typename... Ts>
void print_all(Ts... args)
{
    for (const auto& p : { args... })
        std::cout << p << "\n";
}

template <typename... Ts>
void wrapper(Ts... args)
{
    print_all(0, std::forward<Ts>(args)..., 4);
}

wrapper(1, 2, 3);

// Output:
0
1
2
3
4

wrapper();

// Output:
0
4

This example highlights an interesting point. Considering the call to print_all inside wrapper, if we pass in an empty parameter pack we end up with two commas in a row and yet it’s not a compiler error. Instead the compiler will automatically remove one of the commas, and the expression will evaluate as expected. A bit like reference collapsing.

This is incredibly useful for passing on arguments to object constructors when we don’t know how many parameters the constructor has. This is how std::make_unique is implemented.

Summary

Variadic templates enables us to implement functions and classes with an arbitrary amount of parameters in a type safe way. We use pack expansion to access the values stored in the parameter pack.

  • Ellipsis on the left side of a name denotes a parameter pack
  • Ellipsis on the right side of a name denotes a pack expansion
  • sizeof... return the number of elements in a parameter pack
  • std::tuple is a heterogeneous container of an arbitrary number of types and values
  • Pack expansion can be used in a variety of places, for instance to derive off of multiple base classes

  1. “Classes” here referring to both class and struct [return]
  2. See Florian Weber’s Using Variadic Templates Cleanly for examples not using recursion [return]
  3. See Andrei Alexandrescu’s Modern C++ Design: Generic Programming and Design Patterns Applied for in-depth details on that subject [return]


comments powered by Disqus