Variadic Templates were introduced in the C++11 standard and significantly simplified generic code development. They allow you to create functions and classes that take a variable number of template arguments and function parameters. It’s especially useful when writing universal utilities, frameworks, and libraries that need to work with an arbitrary number of types and parameters.
Why are variadic templates needed?
Before C++11, if you need to pass an arbitrary number of arguments into a function or class using templates, you had to use overload and tricks with macros. All these approaches were often bulky and complicated code.
Variadic templates solve this problem by allowing the use of syntax like the following:
template<typename... Args>
void foo(Args... args) {
// Implementation
}
typename... Args
specifies that the template takes an arbitrary number of types as parameters. Similarly, in function foo(Arg... args)
it’s possible to use a variable number of arguments of any type.
How does it work?
The idea is that the compiler, using mechanisms of unfolding or “unpacking“ parameters, generates corresponding overrides. In the simplest case, we can write a function that prints all passed arguments.
#include <iostream>
// Universal template for base of recursion
template<typename T>
void print(T value) {
std::cout << value << std::endl;
}
// Primary variadic template
template<typename T, typename... Args>
void print(T firstValue, Args... restValues) {
std::cout << firstValue << std::endl;
print(restValues...);
}
int main() {
print(12, 1.56, "Hello, World!");
return 0;
}
Let’s look at the example above:
- First of all, a base function
void print(T value)
is defined that prints one argument and then finishes. - The primary template function
void print(T firstValue, Args... restValues)
is defined, which:-
First it prints
firstValue
-
Then it calls
print(restValues...)
, unpacking the remaining arguments. -
Because of this, when calling
print(12, 1.56, "Hello, World!")
the compiler will recursively generate a chain of calls that correctly process all the parameters.
-
Thus, one piece of code covers many different situations with an arbitrary number of arguments.
Benefits of using Variadic templates
-
Flexibility: you can take any number and type of arguments without sacrificing type safety.
-
Readability: a recursive solution based on variadic templates may be more clear than a series of macros or classical overrides.
-
Support for complex mechanisms of metaprogramming: variadic templates serve as a foundation for many other template tools in modern C++, like
std::tuple
,std::apply
,std::integer_sequence
.
Fold expressions
Fold expressions are a mechanism, introduced in C++17, which simplifies working with variadic templates, allow you to compactly “fold“ all the arguments into one using a special operator. Simply put, fold expression automatically unfolds a list of parameters (Args…
) and applies the specified operator to each element in the list.
The syntax of fold expressions looks like the following:
// Unary left fold
(... op pack)
// Unary right fold
(pack op ...)
// Binary left fold
(init op ... op pack)
// Binary right fold
(pack op ... op init)
-
pack
– an expression that contains an unexpanded pack (for exampleArgs…
), which we unpack -
op
– is an operator that we apply to each element in the list (+
,-
,&&
,,
, etc) -
init
– an expression that does not contain an unexpanded pack and does not contain an operator with precedence lower than cast at the top level
Let’s look at each of these types:
Unary left fold
Syntax is (... op pack)
. This means that the operator op
is applied in a left-associative manner.
(((args1 op args2) op args3) op ...) op argsN
Unary right fold
Syntax is (pack op ...)
. This means that the operator op
is applied in a right-associative manner.
args1 op (args2 op (args3 op (... op argsN)))
Binary left fold
Syntax is (init op ... op pack)
. This means that the operator op
is applied in a left-associative manner with init
(((init op args1) op args2) op ...) op argsN
Binary right fold
Syntax is (pack op ... op init)
. This means that the operator op
is applied in a right-associative manner.
args1 op (args2 op (... op (argsN op init)))
Why do we need fold expressions?
Before fold expressions, we needed to implement a template with a base case and a primary variadic template.
// Base of recursion
template <typename T>
T sumImpl(T value) {
return value;
}
// Primary variadic template
template <typename T, typename... Ts>
T sumImpl(T first, Ts... rest) {
return first + sumImpl(rest...);
}
template <typename... Args>
auto sum(Args... args) {
return sumImpl(args...);
}
This code works, but it requires two functions and recursion. Fold expressions allow for a significantly reduced and simplified implementation:
// Fold expressions
template <typename... Args>
auto sum(Args... args) {
return (args + ...);
}
Now let us return to the example that prints all the passed arguments. Next, we will examine how it can be improved using fold expressions.
template<typename... Args>
void print(Args... args) {
((std::cout << args << std::endl) , ...);
}
int main() {
print(12, 1.56, "Hello, World!");
return 0;
}
In this example (std::cout << args << std::endl), ...)
is an unary right fold, using the comma operator (,
). Expanding this with multiple arguments (12, 1.56, "Hello, World!"
), we get:
(std::cout << 12 << std::endl),
(std::cout << 1.56 << std::endl),
(std::cout << "Hello, World!" << std::endl);
Benefits of using Fold expressions
-
Simplicity: They reduce boilerplate and eliminate the need for additional overrides.
-
No recursion needed: The compiler performs unfolding at compile time, removing manual recursive calls.
-
Improved readability: More concise code is easier to understand and maintain.
Conclusion
Variadic templates are very powerful feature of C++11 that allow elegantly work with an arbitrary number of arguments and simplify the code. They are actively used in standard C++ library (for example in std::tuple
and std::make_unique
) and in many other modern libraries. With the introduction of fold expressions in C++17, working with variadic templates has become even more streamlined. By eliminating recursive template instantiations and reducing boilerplate code, fold expressions enhance readability and efficiency. Having mastered these features empowers developers to write more flexible, expressive, and high-performance C++ programs, ensuring cleaner and more maintainable code.
Try It Yourself on GitHub
If you want to explore these examples hands-on, feel free to visit my GitHub repository where you’ll find all the source files for the code in this article. You can clone the repository, open the code in your favorite IDE or build system, and experiment with different arguments and variations of the functions to see how the compiler expands them. Enjoy playing around with the examples!
Follow me
Github