Skip to content

Variadic Templates & Fold Expressions

Handle any number of template arguments with parameter packs. Master fold expressions for elegant, recursive-free pack processing.

Parameter Packs

A variadic template accepts an arbitrary number of template arguments using a parameter pack. The syntax uses an ellipsis (...). There are two kinds of packs:

1. Template parameter packtemplate — captures zero or more type arguments.
2. Function parameter packvoid f(Ts... args) — captures the corresponding function arguments.

You cannot directly iterate over a pack in a loop. Instead, you expand it using the ... operator. The key insight is that pack expansion applies a pattern to every element of the pack and produces a comma-separated list.

sizeof...(Ts) and sizeof...(args) return the number of elements in the pack as a constexpr std::size_t.

Recursive Pack Expansion (Legacy Pattern)

Before C++17 fold expressions, the standard way to process packs was recursive instantiation: peel off the first element, process it, and recurse on the rest. This requires a base case to terminate recursion.

recursive_variadic.cpp
#include <iostream>

// Base case: no arguments
void print() {
    std::cout << '\n';
}

// Recursive case: process first, recurse on rest
template <typename First, typename... Rest>
void print(const First& first, const Rest&... rest) {
    std::cout << first;
    if constexpr (sizeof...(rest) > 0) {
        std::cout << ", ";
    }
    print(rest...);  // expand rest, recurse
}

int main() {
    print(1, 2.5, "hello", 'A');
    // Output: 1, 2.5, hello, A

    print(); // just a newline
}

Fold Expressions (C++17)

C++17 fold expressions eliminate the need for recursive templates in most cases. A fold expression applies a binary operator across all elements of a parameter pack. There are four forms:

- Unary right fold: (pack op ...) expands to e1 op (e2 op (... op eN))
- Unary left fold: (... op pack) expands to ((e1 op e2) op ...) op eN
- Binary right fold: (pack op ... op init) — includes an initial value
- Binary left fold: (init op ... op pack) — includes an initial value

fold_expressions.cpp
#include <iostream>
#include <string>

// Sum all arguments — unary left fold with +
template <typename... Ts>
auto sum(Ts... args) {
    return (... + args);  // ((a1 + a2) + a3) + ...
}

// Print all arguments — binary left fold with <<
template <typename... Ts>
void print(Ts&&... args) {
    (std::cout << ... << args) << '\n';
    // expands to: ((std::cout << a1) << a2) << a3 ...
}

// Check if ALL arguments satisfy a predicate — unary left fold with &&
template <typename... Ts>
bool all_positive(Ts... args) {
    return (... && (args > 0));  // (a1>0) && (a2>0) && ...
}

// Push multiple elements to a vector — fold over comma operator
#include <vector>
template <typename T, typename... Ts>
void push_all(std::vector<T>& vec, Ts&&... args) {
    (vec.push_back(std::forward<Ts>(args)), ...);
}

int main() {
    std::cout << sum(1, 2, 3, 4, 5) << '\n'; // 15
    print("x=", 10, " y=", 20);               // x=10 y=20
    std::cout << std::boolalpha
              << all_positive(1, 2, 3)   << '\n'  // true
              << all_positive(1, -2, 3)  << '\n'; // false

    std::vector<int> v;
    push_all(v, 10, 20, 30);
    for (int x : v) std::cout << x << ' ';  // 10 20 30
    std::cout << '\n';
}

Pack Expansion Contexts

Pack expansion (...) can appear in many contexts, not just function call arguments:

- Function arguments: f(args...) or f(transform(args)...)
- Template arguments: std::tuple
- Base classes: class Derived : public Bases... { };
- Initializer lists: auto list = { args... };
- Lambda captures: [args...] { } or [...args = std::move(args)] { } (C++20 init-capture)
- Using declarations (C++17): using Bases::operator()...;

The pattern is whatever appears before the .... In f(transform(args)...), the pattern is transform(args), which expands to f(transform(a1), transform(a2), transform(a3)).

This is crucial to understand: f(args)... is not the same as f(args...). The first expands to f(a1), f(a2), f(a3) (three separate calls in a comma expression), while the second expands to f(a1, a2, a3) (one call with three arguments).

std::tuple & std::apply

std::tuple is the standard library's heterogeneous container, built on variadic templates. std::apply (C++17) calls a callable with tuple elements unpacked as arguments. std::index_sequence enables compile-time indexing into tuples.

tuple_apply.cpp
#include <tuple>
#include <iostream>
#include <string>
#include <utility>

// std::apply calls a function with tuple elements as arguments
void greet(const std::string& name, int age) {
    std::cout << "Hello " << name << ", age " << age << '\n';
}

// Print all tuple elements using index_sequence
template <typename Tuple, std::size_t... Is>
void print_tuple_impl(const Tuple& t, std::index_sequence<Is...>) {
    ((std::cout << (Is == 0 ? "" : ", ") << std::get<Is>(t)), ...);
}

template <typename... Ts>
void print_tuple(const std::tuple<Ts...>& t) {
    std::cout << '(';
    print_tuple_impl(t, std::index_sequence_for<Ts...>{});
    std::cout << ")\n";
}

int main() {
    auto person = std::make_tuple(std::string("Alice"), 30);
    std::apply(greet, person);  // Hello Alice, age 30

    auto data = std::make_tuple(1, 3.14, std::string("hello"));
    print_tuple(data);  // (1, 3.14, hello)
}
Pitfall

The most common mistakes with variadic templates:

1. Forgetting the ... — writing args when you mean args... leaves the pack unexpanded, causing a compile error.
2. Expanding in the wrong placef(g(args)...) vs f(g(args...)) have very different meanings. The first calls g once per element; the second calls g with all elements.
3. Empty packs(... + args) with an empty pack is ill-formed for most operators. Use a binary fold with an initial value: (0 + ... + args).
4. sizeof... returns the count, not the size in bytes — sizeof...(args) gives the number of pack elements, not their combined memory size.

Best Practice

Prefer fold expressions over recursive variadic templates whenever possible. They are more concise, easier to read, and often compile faster. Use binary folds with an explicit initial value to handle empty packs safely. Reserve recursive expansion for cases where fold expressions are insufficient — for example, when you need to process elements in pairs, or when processing each element requires different logic.

Key Takeaways
  • Variadic templates use typename... Ts (template parameter pack) and Ts... args (function parameter pack)
  • sizeof...(args) returns the number of elements in a pack at compile time
  • Pack expansion applies a pattern to each element: f(transform(args)...) calls transform on each element
  • Fold expressions (C++17) replace recursive expansion: (... + args) sums all elements
  • Use binary folds with an initial value for safe handling of empty packs
  • std::apply unpacks a tuple as function arguments; std::index_sequence enables compile-time indexing

Quiz — Test Your Knowledge

(20 XP)

1. What does `(... + args)` expand to for `args = {a, b, c}`?

2. What is the difference between `f(args...)` and `f(args)...`?

3. What does `sizeof...(args)` return?