Skip to content
C++
Language
since C++11
Intermediate

Variadic Templates

Variadic templates accept any number of type arguments via parameter packs. Used in tuple, make_unique, std::visit, and any generic factory or forwarding wrapper.

Variadic Templatessince C++11

Templates that accept a variable number of type or non-type arguments via a parameter pack (typename... Ts), expanded with the ... operator at the call or definition site.

Overview

Before C++11, functions accepting arbitrary argument types required either void* casts, va_args, or a combinatorial set of overloads. Variadic templates provide a type-safe, zero-overhead alternative.

The standard library is built on them: std::tuple, std::make_unique, std::variant, std::visit, std::invoke, and all emplace functions use variadic templates internally.

Key terms

TermMeaning
Parameter packtypename... Ts β€” a name standing for zero or more types
Pack expansionTs... β€” expands the pack into a comma-separated list
sizeof...(Ts)Number of types in the pack (C++11)
Fold expression(expr op ...) β€” reduces a pack with an operator (C++17)

Syntax

cpp
// Variadic function template
template<typename... Args>
void print_all(Args&&... args);

// Variadic class template
template<typename... Ts>
struct TypeList {};

// Using sizeof... to get pack size
template<typename... Ts>
constexpr std::size_t count() { return sizeof...(Ts); }

// Pack expansion in function call
template<typename F, typename... Args>
auto call(F f, Args&&... args) {
    return f(std::forward<Args>(args)...);
}

Pack Expansion Contexts

A pack can be expanded anywhere a comma-separated list is grammatically valid:

cpp
template<typename... Ts>
void example(Ts... ts) {
    // In a function call:
    foo(ts...);              // foo(t0, t1, t2)

    // In an initializer list:
    int arr[] = {ts...};     // {t0, t1, t2}

    // In a template argument list:
    std::tuple<Ts...> tup;   // std::tuple<T0, T1, T2>

    // In a base class list:
    // struct Derived : Ts... {};  β€” mixin pattern

    // With an expression applied to each element:
    foo(f(ts)...);           // foo(f(t0), f(t1), f(t2))
}

Recursion Pattern (C++11/14)

Before fold expressions, the standard idiom was base-case + recursive overload:

cpp
// Base case: empty pack
void log() {}

// Recursive case
template<typename First, typename... Rest>
void log(First&& first, Rest&&... rest) {
    std::cout << first << ' ';
    log(std::forward<Rest>(rest)...);   // peel one arg and recurse
}

log(1, "hello", 3.14);   // prints: 1 hello 3.14

Fold Expressions (C++17)

Fold expressions collapse a pack with a binary operator without recursion:

cpp
// Unary right fold: (args op ...)
template<typename... Ts>
auto sum(Ts... ts) { return (ts + ...); }    // t0 + (t1 + (t2 + ...))

// Unary left fold: (... op args)
template<typename... Ts>
auto product(Ts... ts) { return (... * ts); }

// Binary right fold with init value
template<typename... Ts>
void print_space(Ts... ts) { ((std::cout << ts << ' '), ...); }

// With logical operators (short-circuits correctly)
template<typename... Ts>
bool all_positive(Ts... ts) { return ((ts > 0) && ...); }

Common Patterns

Emplace / factory

cpp
template<typename T, typename... Args>
T make(Args&&... args) {
    return T(std::forward<Args>(args)...);
}

Tuple-like index access

cpp
template<std::size_t I, typename... Ts>
auto nth(Ts&&... ts) {
    return std::get<I>(std::tuple<Ts...>(std::forward<Ts>(ts)...));
}

Checking if a type is in a pack

cpp
template<typename T, typename... Ts>
constexpr bool contains_v = (std::is_same_v<T, Ts> || ...);

static_assert(contains_v<int, float, int, double>);

Applying a function to each pack element

cpp
template<typename F, typename... Ts>
void for_each_arg(F&& f, Ts&&... ts) {
    (f(std::forward<Ts>(ts)), ...);   // fold over comma β€” guaranteed left-to-right (C++17)
}

for_each_arg([](auto x){ std::cout << x << '\n'; }, 1, "hello", 3.14);

Common Pitfalls

Order of evaluation in pre-C++17 function calls

Before C++17, argument evaluation order is unspecified. Use fold expressions or std::initializer_list tricks when order matters.

Copying when you should forward

cpp
template<typename... Args>
void bad(Args... args) {      // copies every argument
    sink(args...);
}

template<typename... Args>
void good(Args&&... args) {   // perfect forwarding
    sink(std::forward<Args>(args)...);
}

Pack must be expanded in the same pattern

cpp
template<typename... Ts>
void f(Ts... ts) {
    g(ts..., ts...);   // OK: expands both to t0,t1,...,t0,t1,...
    // g(ts, ts)...;   // error: 'ts' in each position must be one pack
}

Fold Expressions In Depth (C++17)

The existing page introduces fold expressions briefly. This section covers all four syntactic forms, their expansion rules, empty-pack semantics, and the practical patterns you will see most often.

The Four Forms

FormSyntaxExpansion
Unary right fold(pack op ...)(E1 op (E2 op (... op EN)))
Unary left fold(... op pack)(((E1 op E2) op ...) op EN)
Binary right fold(pack op ... op init)(E1 op (... op (EN op init)))
Binary left fold(init op ... op pack)((((init op E1) op E2) op ...) op EN)

The parentheses around the entire expression are required by the grammar. The op must be one of 32 supported binary operators (+, -, *, /, %, ^, &, |, <<, >>, ==, !=, <, >, <=, >=, &&, ||, ,, =, +=, -=, *=, /=, %=, ^=, &=, |=, <<=, >>=, .*, ->*). In a binary fold both operator positions must be the same operator.

Expansion Direction Matters

Left vs right only differs for non-associative or order-sensitive operations:

cpp
// Subtraction is not associative: direction changes the result
template<typename... Ts>
auto sub_right(Ts... ts) { return (ts - ...); }   // t0 - (t1 - t2)

template<typename... Ts>
auto sub_left(Ts... ts) { return (... - ts); }    // (t0 - t1) - t2

sub_right(10, 3, 1);  // 10 - (3 - 1) = 8
sub_left(10, 3, 1);   // (10 - 3) - 1 = 6

For commutative operators (+, *, &&, ||) the direction makes no observable difference.

Empty Pack Semantics

A unary fold over an empty pack is only legal for three operators:

OperatorEmpty-pack result
&&true
||false
,void()

Use a binary fold with an identity init value when you need other operators to work on empty packs:

cpp
template<typename... Ts>
auto sum(Ts... ts) { return (0 + ... + ts); }   // binary left fold; 0 is the init
// sum() returns 0; sum(1,2,3) returns 6

Practical Patterns

cpp
template<typename... Args>
void print_all(Args&&... args) {
    (std::cout << ... << args) << '\n';  // binary left fold over <<
}

print_all("x=", 42, " pi=", 3.14);  // x=42 pi=3.14

Note: this concatenates without separators. Add a space by wrapping in a lambda or using the comma fold:

cpp
template<typename... Args>
void print_sep(Args&&... args) {
    ((std::cout << args << ' '), ...);  // unary left fold over comma
    std::cout << '\n';
}

Push multiple elements into a vector

cpp
template<typename T, typename... Args>
void push_all(std::vector<T>& v, Args&&... args) {
    static_assert((std::is_constructible_v<T, Args&&> && ...),
                  "all args must be constructible to T");
    (v.push_back(std::forward<Args>(args)), ...);
}

std::vector<int> v;
push_all(v, 6, 2, 45, 12);  // v == {6, 2, 45, 12}

Logical all-of / any-of

cpp
template<typename... Ts>
bool all_positive(Ts... ts) { return ((ts > 0) && ...); }

template<typename... Ts>
bool any_zero(Ts... ts) { return ((ts == 0) || ...); }

all_positive(1, 2, 3);   // true
all_positive(1, -1, 3);  // false β€” short-circuits at -1
any_zero(1, 0, 3);       // true  β€” short-circuits at 0

&& and || fold expressions preserve short-circuit evaluation.


sizeof...(pack): Querying Pack Size

sizeof...(pack) returns a compile-time constant of type std::size_t equal to the number of elements in the pack. It works on both type packs and non-type packs.

cpp
template<typename... Ts>
void show_count() {
    constexpr std::size_t n = sizeof...(Ts);
    std::cout << n << " types\n";
}

show_count<int, double, char>();  // 3 types
show_count<>();                   // 0 types

Common uses

Compile-time assertion on pack size

cpp
template<typename... Ts>
struct FixedPair {
    static_assert(sizeof...(Ts) == 2,
                  "FixedPair requires exactly two type arguments");
};

Array sized by pack

cpp
template<typename... Ts>
constexpr auto make_array(Ts&&... ts) {
    using CT = std::common_type_t<Ts...>;
    return std::array<CT, sizeof...(Ts)>{ std::forward<CT>(ts)... };
}

auto arr = make_array(1, 2, 3, 4);  // std::array<int, 4>

Guard against processing empty packs

cpp
template<typename... Ts>
void process(Ts&&... ts) {
    if constexpr (sizeof...(Ts) == 0) {
        return;   // nothing to do
    } else {
        (handle(std::forward<Ts>(ts)), ...);
    }
}

sizeof...(Ts) and sizeof...(ts) are interchangeable when ts is the function-parameter pack corresponding to Ts; both yield the same count.


Variadic Class Templates and the Recursion Pattern

A variadic class template uses the same typename... Ts syntax and is specialised recursively to peel elements one at a time β€” the technique used by the original std::tuple implementations.

The recursive specialisation pattern

cpp
// Base case β€” empty pack
template<typename... Ts>
struct Tuple {};

// Recursive specialisation: peel the first type, store the rest
template<typename T, typename... Rest>
struct Tuple<T, Rest...> {
    T first;
    Tuple<Rest...> rest;

    Tuple(const T& f, const Rest&... r)
        : first(f), rest(r...) {}
};

For Tuple<int, char, std::string> the compiler generates a chain:

cpp
Tuple<int, char, string>
  -> int first
  -> Tuple<char, string> rest
       -> char first
       -> Tuple<string> rest
            -> string first
            -> Tuple<> rest  (base case β€” empty)

Each level is a separate template instantiation, so the compiler must generate N+1 types for a pack of size N. This is the same approach used by many std::tuple implementations, though modern standard libraries often use flatter __tuple_impl layouts to reduce instantiation depth.

Why fold expressions are preferred over recursion in C++17+

When processing a pack at runtime (printing, accumulating, pushing into a container), the recursive function template pattern forces the compiler to instantiate O(N) function templates:

cpp
// C++11/14 style β€” O(N) instantiations
void log() {}

template<typename First, typename... Rest>
void log(First&& first, Rest&&... rest) {
    std::cout << first << ' ';
    log(std::forward<Rest>(rest)...);
}

A C++17 fold expression handles the same task in a single instantiation:

cpp
// C++17 β€” one instantiation, no recursion overhead
template<typename... Args>
void log(Args&&... args) {
    ((std::cout << args << ' '), ...);
}

Advantages of fold expressions over recursion:

  • Less boilerplate β€” no base-case overload required.
  • Faster compile times β€” one template instantiation instead of N.
  • Potentially faster runtime β€” avoids multiple call frames before optimisation.

Recursive class templates (like Tuple above) are still necessary when you need to store heterogeneous data by position, because there is no "fold" equivalent for data layout. For purely computational or side-effectful pack operations, always prefer folds.


Constraining Packs (C++20 Concepts)

Before C++20, constraining every element of a pack required SFINAE with enable_if and a fold over std::conjunction:

cpp
// C++17 SFINAE β€” hard to read, poor diagnostics
template<
    typename... Ts,
    std::enable_if_t<(std::is_integral_v<Ts> && ...), int> = 0
>
auto sum_ints(Ts... ts) { return (ts + ... + 0); }

C++20 concepts make this readable by letting you use a fold expression directly inside a requires clause:

cpp
// C++20 β€” clear intent, better error messages
template<typename... Ts>
    requires (std::integral<Ts> && ...)
auto sum_ints(Ts... ts) { return (ts + ... + 0); }

sum_ints(1, 2, 3);          // OK
sum_ints(1, 2.0, 3);        // error: double does not satisfy std::integral

You can also use the abbreviated function template syntax with a concept constraint in-line:

cpp
template<std::integral... Ts>
auto sum_ints(Ts... ts) { return (ts + ... + 0); }

Constraining individual pack elements with a custom concept

cpp
template<typename T>
concept Printable = requires(T t) {
    { std::cout << t } -> std::same_as<std::ostream&>;
};

template<typename... Ts>
    requires (Printable<Ts> && ...)
void print_all(Ts&&... ts) {
    ((std::cout << ts << '\n'), ...);
}

Mixing concepts with fold expressions in requires clauses

cpp
// All types must be the same
template<typename First, typename... Rest>
    requires (std::same_as<First, Rest> && ...)
void uniform_push(std::vector<First>& v, First first, Rest... rest) {
    v.push_back(first);
    (v.push_back(rest), ...);
}

Comparison: SFINAE vs Concepts for variadic constraints

C++17 SFINAEC++20 Concepts
Syntaxenable_if_t<(Pred<Ts>::value && ...), int> = 0requires (Concept<Ts> && ...)
Error messagesOften cryptic substitution failuresConstraint not satisfied: T does not model Concept
ReusabilityRequires repeating the conjunction at each useNamed concept reused anywhere
Compile timeCan be slower (SFINAE probes all overloads)Typically faster
Supports older compilersYes (C++17+)C++20+ only

C++20 concepts do not remove any expressive power of SFINAE, but they produce cleaner code and better diagnostics. Use concepts for new code targeting C++20 or later.


See Also