In the series:
- Variadic functions – Part 1: va_args and friends
- Variadic functions – Part 2: C++11 variadic templates
- Variadic functions – Part 3: techniques of variadic templates
In the previous article, we have seen an old C-style way of defining the variadic functions – via va_args
macros. We have also learned their weaknesses: runtime cost, strict contract between caller and the callee and lack of type safety. Not to mention the problem of default type promotion!
Fortunately, standards committee thought this through and equipped us in C++11 with variadic templates. Let’s check them out.
Syntax & definitions
First, let’s start with a basic hello world of variadic templates:
#include <iostream>
template <typename T>
T sum(T arg)
{
return arg;
}
template <typename T, typename... Args>
T sum(T first, Args... args)
{
return first + sum(args...);
}
int main(int argc, char* argv[])
{
auto result = sum(1, 3, 5, 7);
std::cout << result << '\n';
return 0;
}
The output of this program is of course:
16
We have two template functions: first taking single parameter and second taking… exactly what? There are three syntax changes compared to the regular templates, that make the second function “variadic”:
typename... Args
which is called template parameter pack, then there is:
Args... args
which is called function parameter pack and finally:
args...
which is called pack expansion.
Template parameter pack defines a list of unspecified (possibly different) types that will be used to instantiate this template function. Function parameter pack is using the same list of types to create the function argument list. The notion of ellipsis in this context is similar to that known from the va_args
functions. The difference and real strength of variadic templates comes in the argument expansion.
Note
The reason for having the first
argument explicitly stated and having an overload will be explained later.
Parameter pack expansion
When compiler sees an ellipsis in the template it automatically expands the expression that uses it into a comma-separated list according to the context. In our example we have three different places where ...
is used. Let’s see in pseudo-code how compiler might unpack them:
Context | Expansion |
---|---|
Template parameter pack | typename... Args -> Arg1, Arg2, Arg3, ..., ArgN |
Function parameter pack | Args... args -> Arg1 arg1, Arg2 arg3, Arg3 arg3, ..., ArgN argN |
Pack expansion | sum(args...) -> sum(arg1, arg2, arg3, ..., argN) |
Note
And now the best of it:
1. All happens at compile time (no runtime cost and allows optimizations)!
2. All types are preserved (ensuring type safety)!
3. We still haven’t used any specific type (no strict contract between caller and callee)!
That makes variadic templates superior to va_arg
in almost every aspect. However, the drawback of this solution is a different approach to implementing the function. This requires a bit of a mindset switch and time to get used to it.
Example explanation
And now is the time to explain the implementation of our adding function. The idea is to use the recursion, extract the first parameter from the argument pack in each call and pass the rest to the next iteration. We add the first unpacked argument to the result of the remaining recursion. This process is repeated until the argument pack has only one element. Then we call the function overload that expects a single template argument. This overload prevents further recursion and is often called the “base case”. You can find the analogy to the popular Factorial<N>
example. Finally this call:
auto result = sum(1, 3, 5, 7);
might result in the following expansion steps made by the compiler:
auto result = sum(1, 3, 5, 7); // 1) variadic case
auto result = 1 + sum(3, 5, 7); // 2) variadic case
auto result = 1 + 3 + sum(5, 7); // 3) variadic case
auto result = 1 + 3 + 5 + sum(7); // 4) base case
auto result = 1 + 3 + 5 + 7; // 5) full expansion
Implementation notes
I said earlier that variadic templates eliminate the strict contract between the called function and its client. This is true to some extent. In C-style variadic functions strict contract means, that function author must specify the exact list of acceptable types along with its order (either in docs or by other parameter like format string). Any difference between implementation and the call-side might lead to undefined behavior.
In case of variadic templates, then only requirement for the caller is to provide the types that support operations used on them within the template body. In our case it is addition. So in other words, we require that for every pair of types from the template parameter pack there is a well-defined operator+()
.
Note
One possible way of ensuring this is to use the concepts from C++20 aka named requirements.
Instantiating template with list of different types
Arguments don’t have to be all of the same type in the template parameter list. We only require the +
operation to be valid for every pair. So the following call is perfectly valid:
sum(true, 0, 4.6, 3.14f);
But the result is quite unexpected:
1
The problem lies in the result type of our template. Lets add some logging to better understand the situation:
#include <iostream>
#include <typeinfo>
template <typename T>
T sum(T arg)
{
return arg;
}
template <typename T, typename... Args>
T sum(T first, Args... args)
{
return first + sum(args...);
}
int main(int argc, char* argv[])
{
auto result = sum(true, 0, 4.6, 3.14f);
std::cout << typeid(result).name() << ": " << result << '\n';
return 0;
}
We have added type name of the result to the output. Now the program prints:
b: 1
This means, that the result variable is bool
. The reason for that, is because our first template instantiation has bool
as the first argument. At that moment the return value is also bool
. And this is the type used in type deduction for the result
variable. The recursion works as expected, but the return values are casted to bool
which can hold only 0
and 1
values.
In C++14 we can ask the compiler to deduce the correct type for us by changing the return types to auto
.
#include <iostream>
#include <typeinfo>
template <typename T>
auto sum(T arg)
{
return arg;
}
template <typename T, typename... Args>
auto sum(T first, Args... args)
{
return first + sum(args...);
}
int main(int argc, char* argv[])
{
auto result = sum(true, 0, 4.6, 3.14f);
std::cout << typeid(result).name() << ": " << result << '\n';
return 0;
}
Now the result is as expected:
d: 8.74
Going further
Recursion is not the only technique used with variadic templates. Our toolbox contains also:
- expression expansion,
- creative trick (read “hack”) with
std::initializer_list
, - C++17 fold expressions,
sizeof...
operator.
But all of that will be covered in the next article. Stay tuned.
Subscribe to get notified about new content
Thanks for taking your time to read it. If you like it or have other opinions, please share it in the comments. You can also subscribe to my newsletter to get latest news about my content. Happy coding and let’s stay connected!
In the series:
- Variadic functions – Part 1: va_args and friends
- Variadic functions – Part 2: C++11 variadic templates
- Variadic functions – Part 3: techniques of variadic templates