Skip to content

Variadic Templates — Parameter Packs, Fold Expressions, Recursive Expansion

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Variadic Templates. We cover key concepts, practical examples, and best practices to help you master this topic.

C++ variadic templates accept any number of template arguments through parameter packs, enabling type-safe printf-style functions, tuple-like types, and compile-time Recursion without runtime overhead.

What You'll Learn

You will write variadic function templates with parameter packs, use recursive expansion to Process individual arguments, apply fold expressions (C++17) for concise pack operations, create variadic class templates like std::tuple, and understand sizeof... for pack size queries.

Why It Matters

Before variadic templates (C++11), varargs functions like printf relied on C-style ... with no type safety. Variadic templates bring compile-time Type Checking to arbitrary-argument functions. They power std::tuple, std::make_unique, std::format, and countless libraries. Combined with C++ fold expressions, they are essential for modern generic programming.

Learning Path

graph LR
    A["44: Template Specialization"] --> B["45: Variadic Templates"]
    B --> C["46: SFINAE & enable_if"]
    C --> D["47: constexpr & consteval"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff

Basic Variadic Function Template

A variadic template uses typename... Args (a template parameter pack) and Args... args (a function parameter pack).

#include <iostream>

// Base case: single argument
template <typename T>
void print(const T& value) {
    std::cout << value << "\n";
}

// Recursive variadic case: one or more arguments
template <typename T, typename... Args>
void print(const T& first, const Args&... rest) {
    std::cout << first << " ";
    print(rest...);  // recursive call with remaining arguments
}

int main() {
    print(1);                          // 1
    print(1, 2.5, "hello", 'x');      // 1 2.5 hello x
    print("only", "one", "call");      // only one call
}

The parameter pack Args captures zero or more types. rest... expands the pack, passing each argument individually to the recursive call. The base case terminates recursion when only one argument remains.

Using sizeof... with Parameter Packs

The sizeof... operator returns the number of elements in a parameter pack at compile time.

#include <iostream>

template <typename... Args>
void countAndPrint(const Args&... args) {
    constexpr size_t numArgs = sizeof...(Args);
    std::cout << "Number of arguments: " << numArgs << "\n";

    // Print all
    ((std::cout << args << " "), ...);
    std::cout << "\n";
}

int main() {
    countAndPrint(1, 2.5, "hello");
    // Number of arguments: 3
    // 1 2.5 hello

    countAndPrint();
    // Number of arguments: 0
    // (empty line)
}

Fold Expressions (C++17)

Fold expressions apply a binary operator over all elements of a parameter pack, eliminating recursion.

#include <iostream>
#include <string>

// Unary right fold (op ... pack)
template <typename... Args>
auto sum(const Args&... args) {
    return (args + ...);  // (arg1 + (arg2 + (arg3 + ...)))
}

// Unary left fold (... op pack)
template <typename... Args>
auto sumLeft(const Args&... args) {
    return (... + args);  // (((arg1 + arg2) + arg3) + ...)
}

// Binary fold (pack op ... init) — handles empty pack
template <typename... Args>
auto sumWithDefault(const Args&... args) {
    return (args + ... + 0);  // 0 is the identity for addition
}

// Print with fold expression (no recursion needed)
template <typename... Args>
void printFold(const Args&... args) {
    ((std::cout << args << " "), ...);
    std::cout << "\n";
}

int main() {
    std::cout << sum(1, 2, 3, 4, 5) << "\n";         // 15
    std::cout << sumLeft(1, 2, 3) << "\n";            // 6

    std::cout << sumWithDefault() << "\n";            // 0 (empty pack)
    std::cout << sumWithDefault(1.5, 2.5) << "\n";    // 4.0

    printFold("fold", "expressions", "are", "concise");
    // fold expressions are concise
}

Fold operators available: +, -, *, /, %, ^, &, |, =, <, >, <<, >>, +=, -=, *=, ,, &&, ||.

Variadic Class Template

Variadic class templates power std::tuple and similar heterogeneous containers.

#include <iostream>
#include <typeinfo>

// Base case: empty tuple
template <typename... Types>
struct Tuple {
    Tuple() = default;
};

// Recursive case: head + tail
template <typename Head, typename... Tail>
struct Tuple<Head, Tail...> : private Tuple<Tail...> {
    Head value;

    Tuple(Head h, Tail... t) : Tuple<Tail...>(t...), value(h) {}

    Head& getHead() { return value; }
    Tuple<Tail...>& getTail() { return *this; }
};

// Helper to get Nth element
template <size_t Index, typename... Types>
struct GetHelper;

template <typename Head, typename... Tail>
struct GetHelper<0, Head, Tail...> {
    static Head& get(Tuple<Head, Tail...>& t) {
        return t.getHead();
    }
};

template <size_t Index, typename Head, typename... Tail>
struct GetHelper<Index, Head, Tail...> {
    static auto& get(Tuple<Head, Tail...>& t) {
        Tuple<Tail...>& tail = t.getTail();
        return GetHelper<Index - 1, Tail...>::get(tail);
    }
};

template <size_t Index, typename... Types>
auto& get(Tuple<Types...>& t) {
    return GetHelper<Index, Types...>::get(t);
}

int main() {
    Tuple<int, double, const char*> t(42, 3.14, "hello");

    std::cout << get<0>(t) << "\n";  // 42
    std::cout << get<1>(t) << "\n";  // 3.14
    std::cout << get<2>(t) << "\n";  // hello
}

Variadic Template with std::index_sequence (C++14)

std::index_sequence helps expand packs into arrays or tuples.

#include <iostream>
#include <array>
#include <utility>

// Convert variadic args to array
template <typename... Args>
std::array<int, sizeof...(Args)> toArray(Args... args) {
    return {static_cast<int>(args)...};
}

// Apply a function to each argument
template <typename Func, typename... Args>
void forEach(Func func, Args&&... args) {
    (func(std::forward<Args>(args)), ...);
}

// Build an array with index_sequence
template <typename T, size_t... Indices>
void printIndices(const std::array<T, sizeof...(Indices)>& arr,
                  std::index_sequence<Indices...>) {
    ((std::cout << Indices << ":" << arr[Indices] << " "), ...);
    std::cout << "\n";
}

template <typename T, size_t N>
void printWithIndices(const std::array<T, N>& arr) {
    printIndices(arr, std::make_index_sequence<N>{});
}

int main() {
    auto arr = toArray(1, 2.5, 3.7f, 'A');
    for (int x : arr) std::cout << x << " ";
    std::cout << "\n";  // 1 2 3 65

    forEach([](auto x) { std::cout << x * 2 << " "; }, 1, 2, 3);
    std::cout << "\n";  // 2 4 6

    std::array<double, 3> darr = {1.1, 2.2, 3.3};
    printWithIndices(darr);  // 0:1.1 1:2.2 2:3.3
}

Practical: Variadic Min/Max

#include <iostream>
#include <algorithm>

template <typename T>
T minimum(T value) {
    return value;
}

template <typename T, typename... Args>
T minimum(T first, Args... rest) {
    T minRest = minimum(rest...);
    return (first < minRest) ? first : minRest;
}

// Using fold expression (C++17)
template <typename... Args>
auto minimumFold(const Args&... args) {
    return (args < ...);  // < is left-associative: (((a < b) < c) < ...)
    // This doesn't work for min! Use min with initializer list instead
}

// Correct fold min: use initializer list
template <typename... Args>
auto minInitList(Args... args) {
    return std::min({static_cast<decltype(args)>(args)...});
}

int main() {
    std::cout << minimum(5, 3, 8, 1, 9) << "\n";    // 1
    std::cout << minimum("apple", "orange", "banana") << "\n";  // apple
    std::cout << minInitList(5, 3, 8, 1, 9) << "\n";  // 1
}

Common Mistakes

Mistake 1: Pack expansion outside of function call context

template <typename... Args>
void wrong(Args... args) {
    args...;  // Error: no context for expansion
}

Packs must be expanded in a function call, initializer list, or fold expression.

Mistake 2: Forgetting base case in recursion

template <typename T, typename... Args>
void print(T first, Args... rest) {
    std::cout << first;
    print(rest...);  // Infinite recursion when rest is empty
}

Always provide a base case (zero or one argument overload) to terminate recursion.

Mistake 3: Confusing pack types with pack values

template <typename... Args>
void func(Args... args) {
    sizeof...(Args);  // Number of types
    sizeof...(args);  // Number of arguments (same value)
}

Mistake 4: Non-portable fold expression with wrong operator

template <typename... Args>
auto sum(Args... args) {
    return (args + ... + 0);  // 0 is wrong for strings
}

The identity value must match the type. std::string needs ""s.

Mistake 5: Pack expansion in wrong order

((std::cout << args << " "), ...);  // prints in order
(... << std::cout << args << " ");  // wrong: << is left-to-right

Use the comma operator with fold expressions for correct left-to-right evaluation.

Practice Questions

  1. What is the output?
template <typename... Args>
auto sum(Args... args) { return (args + ... + 0); }

int main() { std::cout << sum() << " " << sum(1, 2, 3); }

Answer: 0 6

  1. What does sizeof...(Args) return when the pack is empty? Answer: 0 — it works at compile time and can be used in if constexpr checks.

  2. Write a variadic max function using fold expressions. Answer: Use std::max({args...}) — the initializer list approach works because max on an initializer list is variadic.

  3. Can fold expressions work with && and || operators? Answer: Yes, they work with all 32 binary operators in C++, including logical operators.

  4. What is the difference between left fold and right fold? Answer: Left fold (... + args) evaluates as (((a+b)+c)+d). Right fold (args + ...) evaluates as (a+(b+(c+d))). For associative operations like +, they produce the same result.

FAQ

What are variadic templates in C++

Variadic templates accept any number of template arguments via parameter packs. They enable type-safe functions like printf and class templates like tuple.

What is a parameter pack

A parameter pack is a template parameter that accepts zero or more arguments. Packs are expanded with the ... operator in function calls, fold expressions, or initializer lists.

What are fold expressions

Fold expressions (C++17) apply a binary operator over all elements of a parameter pack, eliminating the need for recursive expansion. Example: (args + ...).

How do you handle empty parameter packs

Use a binary fold with an identity value: (args + ... + 0). For type-dependent identities, use if constexpr (sizeof...(Args) == 0).

What is std::index_sequence used for

std::index_sequence generates compile-time index sequences to expand parameter packs into fixed-size containers like std::array or to unpack tuples.

Mini Project

Implement a type-safe format function using variadic templates that replaces {} placeholders with arguments:

#include <iostream>
#include <string>

// Your format function here

int main() {
    std::cout << format("Hello, {}!", "World") << "\n";
    // Hello, World!

    std::cout << format("{} + {} = {}", 3, 4, 7) << "\n";
    // 3 + 4 = 7

    std::cout << format("Pi is approximately {}", 3.14159) << "\n";
    // Pi is approximately 3.14159
}

This project demonstrates real-world variadic template usage. Libraries like std::format (C++20) and fmtlib use this exact mechanism internally.

What's Next

You now wield variadic templates — the foundation of compile-time argument processing. Next, you will dive into SFINAE and enable_if, which control which template overloads participate in resolution based on type properties.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro