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++11Templates 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
| Term | Meaning |
|---|---|
| Parameter pack | typename... Ts β a name standing for zero or more types |
| Pack expansion | Ts... β 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
// 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:
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:
// 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.14Fold Expressions (C++17)
Fold expressions collapse a pack with a binary operator without recursion:
// 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
template<typename T, typename... Args>
T make(Args&&... args) {
return T(std::forward<Args>(args)...);
}Tuple-like index access
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
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
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
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
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
| Form | Syntax | Expansion |
|---|---|---|
| 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:
// 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 = 6For commutative operators (+, *, &&, ||) the direction makes no observable difference.
Empty Pack Semantics
A unary fold over an empty pack is only legal for three operators:
| Operator | Empty-pack result |
|---|---|
&& | true |
|| | false |
, | void() |
Use a binary fold with an identity init value when you need other operators to work on empty packs:
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 6Practical Patterns
Print all values with operator<<
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.14Note: this concatenates without separators. Add a space by wrapping in a lambda or using the comma fold:
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
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
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.
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 typesCommon uses
Compile-time assertion on pack size
template<typename... Ts>
struct FixedPair {
static_assert(sizeof...(Ts) == 2,
"FixedPair requires exactly two type arguments");
};Array sized by pack
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
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
// 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:
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:
// 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:
// 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:
// 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:
// 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::integralYou can also use the abbreviated function template syntax with a concept constraint in-line:
template<std::integral... Ts>
auto sum_ints(Ts... ts) { return (ts + ... + 0); }Constraining individual pack elements with a custom concept
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
// 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 SFINAE | C++20 Concepts | |
|---|---|---|
| Syntax | enable_if_t<(Pred<Ts>::value && ...), int> = 0 | requires (Concept<Ts> && ...) |
| Error messages | Often cryptic substitution failures | Constraint not satisfied: T does not model Concept |
| Reusability | Requires repeating the conjunction at each use | Named concept reused anywhere |
| Compile time | Can be slower (SFINAE probes all overloads) | Typically faster |
| Supports older compilers | Yes (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
- Fold Expressions β C++17 syntax for reducing parameter packs without recursion
- Perfect Forwarding β how
std::forward<Args>(args)...preserves value categories across pack expansion - Type List Idiom β compile-time list manipulation using variadic templates
- Learn: Variadic Templates β step-by-step tutorial with worked examples