Pack Expansion and Parameter Packs
"C++ variadic templates: parameter packs, fold expressions, index_sequence, pack indexing, and compile-time iteration patterns."
Pack Expansionsince C++11A parameter pack is a template parameter that binds zero or more arguments; pack expansion instantiates the surrounding pattern once per element by appending ... to the pattern.
Overview
Parameter packs eliminate the need for overload sets or hand-coded tuple workarounds when a template must accept an arbitrary number of arguments. A pack is declared with ... before the name (typename... Ts) and expanded with ... after the pattern (Ts..., f(args)...). The expansion can appear anywhere a comma-separated list is syntactically valid: function argument lists, template argument lists, base-class lists, initializer lists, and—since C++20—lambda capture lists.
C++17 introduced fold expressions, which reduce a pack over a binary operator without recursion. C++14 brought std::index_sequence for positional iteration over tuples and arrays. C++26 adds direct pack indexing (pack...[N]), making element access as simple as a subscript.
Syntax
// Template parameter packs
template<typename... Ts> // type pack
template<int... Ns> // non-type pack (C++11)
template<auto... Vs> // auto non-type pack (C++17)
template<template<typename>... Cs> // template template pack (C++11)
// Function parameter pack — must follow all non-pack parameters
void f(Ts... args); // value copies
void g(Ts&&... args); // forwarding references (C++11)
// Expansion: append ... after the pattern
f(transform(args)...); // expands to: f(transform(a0), transform(a1), ...)
std::tuple<const Ts*...> // type-level expansion in template arguments
// sizeof...: constexpr count of pack elements
sizeof...(Ts) // type pack
sizeof...(args) // function parameter packExamples
Expansion Contexts
// Base-class list — the "overload" pattern (C++17 using-declaration pack)
template<typename... Visitors>
struct Overload : Visitors... {
using Visitors::operator()...; // C++17: unpack using-declarations
};
// Deduction guide (C++17)
template<typename... Ts> Overload(Ts...) -> Overload<Ts...>;
// Usage:
std::visit(Overload{
[](int x) { std::println("int: {}", x); },
[](std::string s) { std::println("str: {}", s); },
}, some_variant);
// Template argument list
template<typename... Ts>
using tuple_of_vectors = std::tuple<std::vector<Ts>...>;
// C++20: pack expansion in lambda capture with move
template<typename... Args>
auto defer_all(Args&&... args) {
return [...args = std::forward<Args>(args)]() mutable {
(process(std::move(args)), ...);
};
}Fold Expressions (C++17)
Four forms exist, distinguished by fold direction and presence of an initializer:
// Unary right fold: (pack op ...) → E0 op (E1 op (E2 op E3))
// Unary left fold: (... op pack) → ((E0 op E1) op E2) op E3
// Binary right fold: (pack op ... op init) — init is the rightmost operand
// Binary left fold: (init op ... op pack) — init is the leftmost operand
template<typename... Ts>
auto sum(Ts... xs) { return (xs + ... + 0); } // binary right, safe for empty pack
template<typename... Ts>
auto product(Ts... xs) { return (xs * ... + 1); } // identity: 1
// Left-fold chains operator<< correctly (left-associative)
template<typename... Ts>
void print(Ts&&... args) {
(std::cout << ... << args) << '\n'; // C++17 unary left fold
}
// Fold over comma: sequences side effects left-to-right
// (comma operator guarantees sequencing; each sub-expression evaluated in order)
template<typename F, typename... Ts>
void for_each_arg(F&& f, Ts&&... args) {
(f(std::forward<Ts>(args)), ...); // unary right fold over comma
}
// Logical folds short-circuit correctly
template<typename... Ts>
bool all_positive(Ts... xs) { return ((xs > 0) && ...); } // empty → true
template<typename... Ts>
bool any_positive(Ts... xs) { return ((xs > 0) || ...); } // empty → falsestd::index_sequence (C++14)
std::make_index_sequence<N> generates index_sequence<0, 1, ..., N-1> and is the standard mechanism for positional access into heterogeneous containers.
#include <utility>
// Unpack a tuple as individual function arguments
// (std::apply in C++17 does exactly this)
template<typename F, typename Tuple, std::size_t... Is>
decltype(auto) apply_impl(F&& f, Tuple&& t, std::index_sequence<Is...>) {
return std::forward<F>(f)(std::get<Is>(std::forward<Tuple>(t))...);
}
template<typename F, typename Tuple>
decltype(auto) my_apply(F&& f, Tuple&& t) {
using T = std::remove_cvref_t<Tuple>; // C++20
return apply_impl(std::forward<F>(f), std::forward<Tuple>(t),
std::make_index_sequence<std::tuple_size_v<T>>{});
}
// Transform each tuple element with a function, return new tuple
template<typename F, typename Tuple, std::size_t... Is>
auto tuple_transform_impl(F&& f, Tuple&& t, std::index_sequence<Is...>) {
return std::tuple{f(std::get<Is>(std::forward<Tuple>(t)))...};
}
template<typename F, typename... Ts>
auto tuple_transform(F&& f, std::tuple<Ts...>&& t) {
return tuple_transform_impl(std::forward<F>(f), std::move(t),
std::index_sequence_for<Ts...>{});
}
auto t = std::make_tuple(1, 2.0, 3u);
auto doubled = tuple_transform([](auto x) { return x * 2; }, std::move(t));
// tuple<int, double, unsigned>: (2, 4.0, 6)Type-List Manipulation
// Repeat a type N times
template<typename T, std::size_t N,
typename Seq = std::make_index_sequence<N>>
struct repeat_type;
template<typename T, std::size_t N, std::size_t... Is>
struct repeat_type<T, N, std::index_sequence<Is...>> {
template<std::size_t> using Elem = T; // Is drives count, not value
using type = std::tuple<Elem<Is>...>;
};
using four_ints = repeat_type<int, 4>::type; // tuple<int, int, int, int>
// Filter: keep only types satisfying a predicate (via conditional inclusion)
template<template<typename> class Pred, typename... Ts>
using filter_t = decltype(std::tuple_cat(
std::declval<std::conditional_t<Pred<Ts>::value,
std::tuple<Ts>,
std::tuple<>>>()...));
filter_t<std::is_integral, int, double, long, float> // tuple<int, long>C++26: Pack Indexing
Direct subscript syntax for pack elements replaces std::tuple_element gymnastics:
// C++26
template<typename... Ts>
using first_t = Ts...[0];
template<std::size_t I, typename... Ts>
using nth_type = Ts...[I];
template<std::size_t I, typename... Ts>
constexpr decltype(auto) nth(Ts&&... xs) noexcept {
return std::forward<Ts...[I]>(xs...[I]);
}
first_t<int, double, char> // int
nth_type<2, int, double, char> // char
nth<1>(10, 20, 30) // 20
// Combining with index_sequence for slicing (C++26)
template<std::size_t Start, typename... Ts, std::size_t... Is>
auto slice_from_impl(std::index_sequence<Is...>, Ts&&... xs) {
return std::tuple<Ts...[Start + Is]...>{xs...[Start + Is]...};
}
template<std::size_t Start, typename... Ts>
auto slice_from(Ts&&... xs) {
return slice_from_impl<Start>(
std::make_index_sequence<sizeof...(Ts) - Start>{},
std::forward<Ts>(xs)...);
}Best Practices
- Prefer fold expressions over recursive templates (C++17). They compile faster and avoid instantiation depth limits imposed by
-ftemplate-depth. - Use binary fold with identity for arithmetic operators.
(xs + ...)is ill-formed over an empty pack;(xs + ... + 0)is always well-formed. - Use
std::apply(C++17) rather than hand-rolling index_sequence indirection when calling a function with a tuple's elements. - Constrain packs with concepts (C++20):
template<std::integral... Ts>produces clearer diagnostics than unconstrained packs and prevents silent misuse. - Move-capture packs in long-lived lambdas (C++20):
[...args = std::move(args)]()avoids dangling references when the pack escapes the enclosing function.
Common Pitfalls
Unary fold over empty pack. Unary folds over most operators—+, *, |, &, <<—are ill-formed when the pack is empty. Only && (identity true), || (identity false), and , (yields void) are safe. Use binary fold with an explicit identity element to handle the empty case.
Unspecified evaluation order in function argument expansion. Expanding a pack in a function's argument list does not guarantee evaluation order:
// Evaluation order of side_effect(a0), side_effect(a1), ... is unspecified
f(side_effect(args)...);
// Guaranteed left-to-right: fold over comma
(f(args), ...);
// Also guaranteed (initializer-list): left-to-right with discarded result
std::initializer_list<int>{(f(args), 0)...};Dependent pack in unevaluated context. A bare pack name cannot appear in sizeof, typeid, or decltype without being expanded. Use sizeof...(pack) for the count, or wrap in a tuple for type-level inspection.
Mixing pack and non-pack in the same pattern. When multiple packs appear in one expansion (f(As..., Bs...) is ill-formed unless both packs have the same length—the compiler expands them in lockstep, like std::zip).
See Also
- Fold Expressions — full operator table and associativity rules
- Variadic Templates — template parameter packs vs. function parameter packs
- Concepts — constraining packs with requires-clauses
- std::tuple — canonical heterogeneous container for pack storage