paint-brush
C++ Metaprogramming: Variadic Templates & Fold Expressions—Now with 100% Less Headachesby@Zool
252 reads New Story

C++ Metaprogramming: Variadic Templates & Fold Expressions—Now with 100% Less Headaches

by Vladislav AgMarch 6th, 2025
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Variadic templates in C++11 enable functions and classes to accept any number of arguments, reducing complexity. Fold expressions (C++17) simplify variadic operations further by eliminating recursion and improving readability. This guide covers code examples, use cases, and optimizations for modern C++ development.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - C++ Metaprogramming: Variadic Templates & Fold Expressions—Now with 100% Less Headaches
Vladislav Ag HackerNoon profile picture
0-item


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

  • 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 example Args…), 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

LinkedIn

Github