Skip to content
C++

Variadic Templates

Functions and classes that accept any number of arguments — with full compile-time type safety. Variadic templates power std::make_tuple, std::variant, std::bind, and most of the modern STL. Understanding them unlocks the ability to write genuinely generic library code.

The problem they solve

Before C++11, writing functions that accepted an arbitrary number of arguments required C-style variadic functions using va_list, va_start, and va_arg. These had two fundamental weaknesses: they are not type-safe (the compiler has no idea what types you pass), and they need a runtime mechanism to count arguments (a format string like printf, or a sentinel value). A single type mismatch is undefined behaviour with no diagnostic.

Another workaround was to write overloads for one, two, three, … N arguments. This is tedious, still limits N, and produces code that is impossible to maintain.

Variadic templates eliminate both problems entirely. The compiler knows the exact type of every argument. The argument count is a compile-time constant you can query with sizeof.... No runtime overhead, no undefined behaviour, no arbitrary limit.

// C-style: type unsafe, runtime counting, UB on mismatch
#include <cstdarg>
int sum_c(int count, ...) {
    va_list args;
    va_start(args, count);
    int total = 0;
    for (int i = 0; i < count; ++i)
        total += va_arg(args, int);  // silent UB if you pass a double
    va_end(args);
    return total;
}
sum_c(3, 1, 2, 3);         // OK
sum_c(3, 1, 2, 3.5);      // UB — no diagnostic

// C++11 variadic template: fully type-safe, no count needed
template <typename... Args>
int sum(Args... args);     // handles 0 or more args, any types

The three roles of ...

The ellipsis appears in three distinct grammatical positions, each meaning something different. Confusing them is the most common source of variadic template errors.

PositionSyntaxMeaning
Template parameter pack declarationtypename... ArgsDeclares a pack of zero or more type parameters
Function parameter pack declarationArgs... argsDeclares a pack of function parameters with those types
Pack expansionargs...Expands the pack — repeats the pattern for each element
//                    ↓ template parameter pack (declares Args)
template <typename T, typename... Args>
//                              ↓ function parameter pack (one value per type)
void print_all(T first, Args... rest) {
    std::cout << first << ' ';
    //                ↓ pack expansion (expand rest into comma-separated args)
    print_all(rest...);
}
// print_all(1, 2.5, "hi") expands to print_all(2.5, "hi"), then print_all("hi")

The classic recursion pattern

Because a pack is not an array you can loop over, processing its elements requires compile-time recursion. Each call peels off one element (the head) and recursively processes the remainder (the tail). You need a base case overload to terminate the recursion when the pack is empty. The compiler selects between overloads at compile time — this is not runtime recursion.

// Base case: called when only one argument remains
template <typename T>
T min_val(T a) { return a; }

// Recursive case: peel off 'a', recurse on the rest
template <typename T, typename... Args>
T min_val(T a, Args... args) {
    return std::min(a, min_val(args...));
}

auto result = min_val(5, 3, 8, 1, 9);  // → 1
// The compiler generates:
//   min_val(5, min_val(3, min_val(8, min_val(1, min_val(9)))))
// All resolved at compile time with optimization

C++17 introduced if constexpr, which lets you write the base case inside the same function using sizeof...(args) — no separate overload needed.

// Single function, base case via if constexpr (C++17)
template <typename T, typename... Args>
T sum(T a, Args... args) {
    if constexpr (sizeof...(args) == 0)
        return a;               // base case: nothing left to add
    else
        return a + sum(args...);
}

sum(1, 2, 3, 4, 5);   // → 15
sum(1.5, 2.5);         // → 4.0
sum(42);               // → 42 (single arg)

sizeof...(pack) — compile-time count

The sizeof... operator (note: not the same as sizeof) returns the number of elements in a parameter pack as a compile-time constexpr std::size_t. It can be applied to both template parameter packs and function parameter packs. The parentheses are required.

template <typename... Args>
void describe(Args... args) {
    std::cout << "Type count:  " << sizeof...(Args) << '\n';  // template pack
    std::cout << "Value count: " << sizeof...(args) << '\n';  // function pack
    // Both always equal; pick whichever reads more clearly
}

describe(1, 'a', 3.14, std::string{"hi"});
// Type count:  4
// Value count: 4

// Common use: static_assert on arity
template <typename... Args>
void exactly_two(Args... args) {
    static_assert(sizeof...(Args) == 2, "exactly_two() requires exactly 2 arguments");
}

Where packs can be expanded

Pack expansion (pack...) is not just for function call arguments. The compiler allows expansion in several syntactic contexts. In each case, the expansion repeats the entire pattern — not just the pack name — for every element. This makes non-trivial transformations expressible in a single line.

// 1. Function call arguments (most common)
template <typename... Args>
void forward_all(Args&&... args) {
    target(std::forward<Args>(args)...);
    // expands to: target(std::forward<A1>(a1), std::forward<A2>(a2), ...)
}

// 2. Initializer lists
template <typename... Args>
auto make_vec(Args... args) {
    return std::vector<std::common_type_t<Args...>>{args...};
}

// 3. Base class list
template <typename... Bases>
struct MultiDerived : public Bases... {
    using Bases::operator()...;  // also: using declarations
};

// 4. Template argument list
template <typename... Args>
using tuple_of = std::tuple<Args...>;

// 5. Lambda capture (C++20)
template <typename... Args>
auto capture_all(Args... args) {
    return [args...]() { return sum(args...); };
}

Fold expressions (C++17)

The recursion + base-case overload pattern works, but it is verbose and the overload can leak into external code. C++17 introduced fold expressions — a compact syntax that applies a binary operator across all elements of a pack, eliminating the need for recursion in the most common cases. There are four forms:

FormSyntaxExpansion
Unary right fold(pack op ...)a1 op (a2 op (a3 op a4))
Unary left fold(... op pack)((a1 op a2) op a3) op a4
Binary right fold(pack op ... op init)a1 op (a2 op (a3 op init))
Binary left fold(init op ... op pack)((init op a1) op a2) op a3

Unary folds on an empty pack are only well-formed for three operators: && (yields true), || (yields false), and the comma operator (yields void()). For everything else, use a binary fold with an identity-element initializer.

// Unary left fold: replaces two-overload recursion entirely
template <typename... T>
auto sum(T... args) { return (... + args); }
sum(1, 2, 3, 4, 5);  // → 15
// expands to: ((((1 + 2) + 3) + 4) + 5)

// Binary left fold: safe for empty packs (0 is the additive identity)
template <typename... T>
auto safe_sum(T... args) { return (0 + ... + args); }
safe_sum();          // → 0  (empty pack is fine)
safe_sum(1, 2, 3);  // → 6
// Print all arguments with spaces (comma fold)
template <typename... Args>
void print_all(Args&&... args) {
    ((std::cout << args << ' '), ...);
    std::cout << '\n';
}
print_all(1, 2.5, "hello", 'X');
// prints: 1 2.5 hello X

// Push multiple values into a vector
template <typename T, typename... Args>
void push_back_many(std::vector<T>& v, Args&&... args) {
    (v.push_back(std::forward<Args>(args)), ...);
}
std::vector<int> v;
push_back_many(v, 1, 2, 3, 4, 5);  // v = {1,2,3,4,5}
// Logical folds — check properties across all types
template <typename... Args>
bool all_positive(Args... args) { return ((args > 0) && ...); }

template <typename... Args>
bool any_negative(Args... args) { return ((args < 0) || ...); }

all_positive(1, 2, 3);   // → true
all_positive(1, -1, 3);  // → false
any_negative(1, -2, 3);  // → true

Variadic class templates

Classes use the same parameter pack syntax. Where function templates recurse via overload resolution, class templates recurse via partial specialization. The primary template defines the general structure; the specialization handles the base case. std::tupleis the canonical example — it stores N heterogeneous values by inheriting from a chain of single-element wrappers.

// Simplified tuple — demonstrates recursive inheritance pattern
template <typename T, typename... Rest>
struct tuple : public tuple<Rest...> {       // inherit from smaller tuple
    T value;
    tuple(T v, Rest... rest) : tuple<Rest...>(rest...), value(v) {}
};

// Base case: tuple with single element
template <typename T>
struct tuple<T> {
    T value;
    tuple(T v) : value(v) {}
};

tuple<int, double, std::string> t(42, 3.14, "hello");
// Layout: tuple<std::string> { "hello" }
//         tuple<double, std::string> inherits above, adds 3.14
//         tuple<int, double, std::string> inherits above, adds 42

Accessing elements at a given index requires a helper that descends the inheritance chain N times. This is what std::get<N>(t) does internally.

// Type-at-index metafunction
template <size_t N, typename T, typename... Rest>
struct nth_type : nth_type<N - 1, Rest...> {};  // descend

template <typename T, typename... Rest>          // base case: N == 0
struct nth_type<0, T, Rest...> { using type = T; };

template <size_t N, typename... Args>
using nth_type_t = typename nth_type<N, Args...>::type;

// Usage
nth_type_t<0, int, double, char>  // → int
nth_type_t<2, int, double, char>  // → char

STL types built on variadic templates

std::tuple<Ts...>

Heterogeneous fixed-size sequence. std::get<N>(t) for element access, std::tie for unpacking.

std::variant<Ts...>

Type-safe union — holds exactly one of the listed types at any time. std::visit for dispatch.

std::make_tuple / std::make_from_tuple

Variadic function templates that deduce types and construct tuples without specifying template args.

std::apply(f, tuple)

Calls f with the elements of tuple as individual arguments — uses index_sequence internally.

std::index_sequence<0,1,2,...>

Compile-time sequence of integers. std::make_index_sequence<N> generates 0…N-1.

// std::apply: call a function with tuple elements as separate args
auto coords = std::make_tuple(3.0, 4.0);
auto dist = std::apply([](double x, double y) {
    return std::sqrt(x * x + y * y);
}, coords);  // → 5.0

// std::index_sequence pattern for iterating over tuple elements
template <typename Tuple, size_t... Is>
void print_tuple_impl(const Tuple& t, std::index_sequence<Is...>) {
    ((std::cout << std::get<Is>(t) << ' '), ...);
}

template <typename Tuple>
void print_tuple(const Tuple& t) {
    print_tuple_impl(t, std::make_index_sequence<std::tuple_size_v<Tuple>>{});
}

print_tuple(std::make_tuple(1, 2.5, "hello"));  // 1 2.5 hello

Variadic alias and variable templates

The variadic mechanism is not limited to functions and classes. Alias templates and variable templates can also take parameter packs, useful for partial binding of template arguments and compile-time aggregation.

// Variadic alias template: partially bind the first type parameter
template <typename... Args>
using int_tuple = std::tuple<int, Args...>;  // first arg is always int

int_tuple<double, std::string>  // → std::tuple<int, double, std::string>

// Another common use: alias for std::enable_if_t chains
template <typename T, typename... Req>
using requires_all = std::enable_if_t<(std::is_base_of_v<Req, T> && ...)>;
// Variadic variable template: compile-time aggregate
template <int... Values>
constexpr int Sum = (... + Values);

constexpr int total = Sum<1, 2, 3, 4, 5>;   // → 15 (no runtime cost)
constexpr int product_like = Sum<10, 20>;    // → 30

// More useful: any-of / all-of over a type list
template <typename T, typename... Types>
constexpr bool is_any_of = (std::is_same_v<T, Types> || ...);

static_assert(is_any_of<int, float, int, double>);   // true
static_assert(!is_any_of<char, float, int, double>); // false

Practical pattern: type-safe printf

The motivating example for variadic templates in the C++ standard was replacing C's printf with a type-safe equivalent. Here is a minimal version that validates format specifiers against actual argument types at compile time — something C's printf cannot do.

// Base case: no more arguments
void safe_printf(const char* fmt) {
    while (*fmt) {
        if (*fmt == '%' && *++fmt != '%')
            throw std::runtime_error("too few arguments");
        std::cout << *fmt++;
    }
}

// Recursive case: consume one argument per format specifier
template <typename T, typename... Args>
void safe_printf(const char* fmt, T first, Args... rest) {
    while (*fmt) {
        if (*fmt == '%' && *++fmt != '%') {
            std::cout << first;    // type-safe: operator<< is resolved at compile time
            safe_printf(++fmt, rest...);
            return;
        }
        std::cout << *fmt++;
    }
    throw std::runtime_error("too many arguments");
}

safe_printf("Hello %s, you are % years old\n", std::string{"Alice"}, 30);
// Works; the types flow through operator<< — no format-specifier mismatch possible

Quick reference

IdiomSyntaxWhen to use
Declare type packtypename... ArgsTemplate parameter list
Declare value packArgs... argsFunction parameter list
Expand packargs...Call site, initializer list, base class list
Count packsizeof...(args)Static asserts, if constexpr guards
Unary fold(... op pack)Reduce all elements, C++17
Binary fold (empty-safe)(init op ... op pack)Same, but handles empty pack
Pattern fold((f(args)), ...)Apply f to each element via comma fold
Type-at-indexnth_type_t<N, Ts...>Metafunction to extract Nth type
Index sequencestd::make_index_sequence<N>Iterate over tuple elements
Sign in to track progress