C++ Variadic Templates
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) {
// ...
}
}
Outputs:
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;
Outputs:
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);
Outputs:
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);
Outputs:
0
1
2
3
4
wrapper();
Outputs:
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 packstd::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
-
“Classes” here referring to both
class
andstruct
↩︎ -
See Florian Weber’s Using Variadic Templates Cleanly for examples not using recursion ↩︎
-
See Andrei Alexandrescu’s Modern C++ Design: Generic Programming and Design Patterns Applied for in-depth details on that subject ↩︎