Introduction to C++ Variadic Templates
Posted on January 27, 2016This is a friendly introduction to variadic templates (and thereby, variadic functions) in C++. We can use variadic functions to write functions that accept an arbitrary number of arguments.
First, let’s focus on the syntax: how do we read
a variadic template? What are the different syntactic
elements, and what are they called? Let’s look at a
simple example. We’ll create a variadic function ignore
that does nothing (it ignores its arguments).
-
We use
typename... Ts
to declareTs
as a so-called template parameter pack. You’ll often see these called as e.g.Ts
, as in, multipleT
s. Other common names areArgs
, orRest
. The ellipsis (...
) operator is used here to declare thatTs
truly is a template parameter pack. -
Our function signature accepts a closely-related function parameter pack – in other words, a bag of parameters, whose types are given by the aforementioned template parameter pack. It is declared as
Ts... ts
, with the ellipsis operator used to indicate thatTs
does refer to a template parameter pack.
You can mentally unwrap the above definition of ignore
as:
This also makes it clear that each type in the template parameter pack can be different – just to be concrete, calling:
has the effect of instantiating our templated function with
types ignore<int, double, bool>(1, 2.0, true)
.
An important side note: it’s possible for a template parameter pack to contain 0 types. This might be somewhat obvious, but it’ll become more important later.
Okay, we know what they are – but how do we use them? How do we implement a variadic function that does some real work?
Let’s start by implementing a variadic sum – it’ll take
a bunch of arguments, and just attempt to add them all up.
We’ll assume that the function returns a double
for now,
although in practice you’d probably like the result to
depend on the input types (e.g. adding two int
s should
probably return an int
).
If you’re like me, the first thing you probably wished you could write was something like:
Unfortunately, this won’t do. Here’s what I get from clang
:
error: expression contains unexpanded parameter pack 'ts'
for (auto el : ts)
^~
When it comes to handling variadic functions, you can’t think in the standard ‘iterative’ C++ style. You need to write such functions recursively – with a ‘base’ case, and a ‘recursive’ case that reduces, eventually, into a ‘base’ case. This implies a separate function for each case.
Unfortunately, none of this makes sense until you see an example. So let’s start with a working example, and then break it down.
We have our ‘base’ case, accepting one argument T
,
and our ‘recursive’ case, accepting one or more arguments
T
and Rest
. (Recall that a template parameter pack can be empty!)
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.
-
The compiler generates code for
sum(1.0, 2.0, 3.0)
. There are two competing overloads forsum
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. -
Type deduction is performed – the compiler deduces
T = double
, and puts the rest in our parameter pack, withRest = <double, double>
. -
The compiler generates code for
t + sum(rest...)
. It sees the recursive call tosum(rest...)
. Note the use of...
to ‘unpack’ the template argument – this has the effect of transformingsum(rest...)
tosum(2.0, 3.0)
. -
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. -
Type deduction is performed – the compiler deduces
T = double
, andRest = <double>
. It’s subtle, but notice that we have now unpacked our original<double, double>
pack toT = double
andRest = <double>
. -
The compiler generates code for
t + sum(rest...)
. It sees the recursive call tosum(rest...)
– this time, withsum(rest...)
expanding to simplysum(3.0)
. -
The compiler generates code for
sum(3.0)
. We have now finally hit our base case: the overload taking onlyT
is more specialized, relative to the overload taking bothT
andTs...
. The compiler generates code for the base case, and we’re done with the recursion.
All in all, the expression expands in the following way (using indices
to distinguish the various t
s produced on expansion):
t0 + sum(rest...); // initial state
t0 + sum(t1, t2); // unpack 'rest...' as 't1, t2'
t0 + (t1 + sum(rest...)); // replace 'sum(t1, t2)' with code
t0 + (t1 + sum(t2)); // unpack 'rest...' as 't2'
t0 + (t1 + (t2)); // 'sum(t2)' --> base case!
Or, if you prefer seeing it with numbers,
sum(1.0, 2.0, 3.0);
1.0 + sum(2.0, 3.0);
1.0 + (2.0 + sum(3.0));
1.0 + (2.0 + (3.0));
And there we have it. Although our sum implementation makes use of compile-time recursion, the end result is a linear addition of code. 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.
Using variadic functions does indeed require a bit of a change in mindset, and is somewhat more verbose (given the amount of code required to write the base and recursive cases). However, these are the main tools you need for performing computation with variadics: unpack and reduce.
Let’s make our function a little bit more clever: how about instead of computing the sum, we compute something like:
Notice the expression square(rest)...
. Recall that the ...
operator will expand an entire expression, so for example,
when it’s called with square(4.0, 6.0)...
, the compiler expands
this as square(4.0), square(6.0)
. Let’s trace the expansion of
the resulting code:
power_sum(2.0, 4.0, 6.0);
2.0 + power_sum(square(rest)...);
2.0 + power_sum(square(4.0), square(6.0));
2.0 + (square(4.0) + power_sum(square(rest)...))
2.0 + (square(4.0) + power_sum(square(square(6.0)));
2.0 + (square(4.0) + (square(square(6.0))))
It’s important to note that ...
can be used to expand a
whole expression containing a parameter pack – this is
what makes it so powerful! On the downside, this expansion
can only occur in certain contexts, e.g. within a function
call. Clever use of ...
expansion can allow you to avoid
recursion in some cases, although some extra tricks beyond
the scope of this article are required.
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.
As an aside, I tried to sidestep issues related to pass-by-reference vs. pass-by-value vs. so-called ‘perfect forwarding’, as they’re somewhat orthoginal to understanding variadic functions alone. However, if you want to learn more, you should check out Perfect forwarding and universal references in C++.
Further Reading
These are the resources I found most helpful when getting familiar with variadic templates, and will also outline some other techniques for effective use / implementations.
- Using Variadic Templates Cleanly — focuses on using
...
to unpack expressions, helping to avoid recursion. - Variadic Templates in C++ — goes deeper, discussing variadic data structures and more.
- CppReference — the ‘standardese’ description of parameter packs, with some helpful examples.
Addendum
Please note that I’m not a C++ expert; there’s likely a number of holes in my understanding, but what I divulge here will still hopefully be useful to other beginners.