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:

  1. First of all, a base function void print(T value) is defined that prints one argument and then finishes.
  2. 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

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)

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

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

LinkedIn

Github