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

Pack Expansion and Parameter Packs

"C++ variadic templates: parameter packs, fold expressions, index_sequence, pack indexing, and compile-time iteration patterns."

Pack Expansionsince C++11

A 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

cpp
// 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 pack

Examples

Expansion Contexts

cpp
// 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:

cpp
// 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 → false

std::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.

cpp
#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

cpp
// 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:

cpp
// 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:

cpp
// 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