Skip to content

Parameter Packs and Variadic Templates

A variadic template accepts a variable number of template arguments via a parameter pack [N4950 §13.7.3]. Parameter packs come in two forms: type parameter packs and non-type parameter Packs. Combined with pack expansion syntax and perfect forwarding, they enable type-safe Operations on arbitrary numbers of arguments.

Variadic Function Templates and Parameter Packs

Section titled “Variadic Function Templates and Parameter Packs”

A variadic template accepts a variable number of template arguments via a parameter pack [N4950 §13.7.3]. Parameter packs come in two forms: type parameter packs and non-type parameter Packs.

#include <iostream>
#include <type_traits>
// sizeof... returns the number of elements in a pack [N4950 §8.3.3]
template <typename... Ts>
constexpr std::size_t count_types() {
return sizeof...(Ts);
}
int main() {
static_assert(count_types<>() == 0);
static_assert(count_types<int>() == 1);
static_assert(count_types<int, double, char>() == 3);
std::cout << count_types<int, double, char, long>() << "\n"; // 4
}

A pack expansion pattern... expands the pattern by substituting each element of the pack [N4950 §13.7.3]. The expansion can appear in various contexts:

  • Function argument lists: f(args...)
  • Template argument lists: Tuple<Types...>
  • Initializer lists: {args...}
  • Base class lists: class Derived : Bases...
#include <iostream>
#include <tuple>
#include <utility>
#include <string>
// Recursive variadic print
void print() {
std::cout << "\n";
}
template <typename T, typename... Rest>
void print(T first, Rest... rest) {
std::cout << first;
if constexpr (sizeof...(rest) > 0) {
std::cout << ", ";
}
print(rest...);
}
// Forwarding reference + variadic: perfect forwarding wrapper
template <typename... Args>
auto make_tuple_wrapper(Args&&... args) {
return std::make_tuple(std::forward<Args>(args)...);
}
// Count occurrences of T in Ts...
template <typename T, typename... Ts>
struct count_occurrences;
template <typename T>
struct count_occurrences<T> : std::integral_constant<int, 0> {};
template <typename T, typename First, typename... Rest>
struct count_occurrences<T, First, Rest...>
: std::integral_constant<int,
(std::is_same_v<T, First> ? 1 : 0)
+ count_occurrences<T, Rest...>::value> {};
int main() {
print(1, "hello", 3.14, "x');
// Output: 1, hello, 3.14, x
auto t = make_tuple_wrapper(42, std::string{"world"}, 3.14);
std::cout << std::get<0>(t) << "\n"; // 42
std::cout << std::get<1>(t) << "\n"; // world
static_assert(count_occurrences<int, int, double, int, char>::value == 2);
static_assert(count_occurrences<double, int, double, int>::value == 1);
}

Variadic make_unique (Custom Implementation)

Section titled “Variadic make_unique (Custom Implementation)”
#include <iostream>
#include <memory>
#include <utility>
template <typename T, typename... Args>
std::unique_ptr<T> my_make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
struct Widget {
int x, y;
Widget(int a, int b) : x(a), y(b) {
std::cout << "Widget(" << x << ", " << y << ")\n";
}
};
int main() {
auto w = my_make_unique<Widget>(10, 20);
std::cout << w->x << ", " << w->y << "\n"; // 10, 20
}
## Variadic Class Templates

Parameter packs are not limited to function templates. A variadic class template accepts a pack Of type parameters, enabling type-safe heterogeneous containers and mixin-style composition [N4950 §13.7.3].

#include <iostream>
#include <tuple>
#include <type_traits>
// Variadic class template: holds one of each type
template <typename... Types>
struct TypeHolder {
static constexpr std::size_t count = sizeof...(Types);
template <std::size_t I>
using type_at = std::tuple_element_t<I, std::tuple<Types...>>;
};
int main() {
using Holder = TypeHolder<int, double, char, long>;
static_assert(Holder::count == 4);
static_assert(std::is_same_v<Holder::type_at<0>, int>);
static_assert(std::is_same_v<Holder::type_at<2>, char>);
std::cout << "Types: " << Holder::count << "\n";
}

Variadic inheritance uses pack expansion in the base class list [N4950 §13.7.3]:

#include <iostream>
#include <string>
struct Printer {
void print() const { std::cout << "Printer\n"; }
};
struct Logger {
void log() const { std::cout << "Logger\n"; }
};
struct Serializer {
void serialize() const { std::cout << "Serializer\n"; }
};
// Variadic mixin composition: each base provides a capability
template <typename... Mixins>
class Component : public Mixins... {
public:
// Inherit all constructors from each mixin
using Mixins::Mixins...;
void run_all() const {
// Each call resolves via the appropriate base
(void(Printer::print), ...); // only compiles if Printer is in Mixins...
(void(Logger::log), ...);
(void(Serializer::serialize), ...);
}
};
int main() {
Component<Printer, Logger> c1;
c1.print();
c1.log();
Component<Serializer> c2;
c2.serialize();
}

sizeof...(pack) returns the number of elements in a parameter pack as a std::size_t [N4950 §8.3.3]. It is a constant expression usable in if constexpr``static_assertAnd template Metaprogramming. It works on both type packs and non-type packs.

#include <iostream>
#include <type_traits>
// Compile-time type list length
template <typename... Ts>
struct TypeList {
static constexpr std::size_t length = sizeof...(Ts);
};
// Conditional: return first type if pack has exactly one element
template <typename... Ts>
auto first_or_default() {
if constexpr (sizeof...(Ts) == 1) {
return 42; // single element case
} else if constexpr (sizeof...(Ts) == 0) {
return 0; // empty pack
} else {
return -1; // multiple elements
}
}
int main() {
static_assert(TypeList<>::length == 0);
static_assert(TypeList<int, double>::length == 2);
static_assert(first_or_default<>() == 0);
static_assert(first_or_default<int>() == 42);
static_assert(first_or_default<int, double>() == -1);
std::cout << "All assertions passed\n";
}

There is no built-in “get the Nth type of a pack” operator in C++. The standard technique is to Convert the pack to std::tuple and use std::tuple_element_t for type indexing or std::get for Value indexing [N4950 §22.4.6].

#include <iostream>
#include <tuple>
#include <string>
#include <type_traits>
// Type indexing: get the Nth type from a parameter pack
template <std::size_t I, typename... Ts>
using pack_element_t = std::tuple_element_t<I, std::tuple<Ts...>>;
// Value indexing: get the Nth value from a pack of values
template <std::size_t I, typename... Ts>
decltype(auto) get_pack_element(Ts&&... args) {
return std::get<I>(std::forward_as_tuple(std::forward<Ts>(args)...));
}
// Apply a function to the Nth argument
template <std::size_t I, typename Fn, typename... Ts>
decltype(auto) apply_at(Fn&& fn, Ts&&... args) {
return std::forward<Fn>(fn)(get_pack_element<I>(std::forward<Ts>(args)...));
}
int main() {
static_assert(std::is_same_v<pack_element_t<0, int, double, char>, int>);
static_assert(std::is_same_v<pack_element_t<1, int, double, char>, double>);
static_assert(std::is_same_v<pack_element_t<2, int, double, char>, char>);
auto val = get_pack_element<1>(10, std::string{"hello"}, 3.14);
std::cout << val << "\n"; // hello
auto result = apply_at<0>([](int x) { return x * 2; }, 21, "test", 1.0);
std::cout << result << "\n"; // 42
}

Pack expansion pattern... can appear in many syntactic positions [N4950 §13.7.3]. Each context Substitutes each pack element into the pattern and produces a comma-separated list of expansions:

#include <iostream>
#include <vector>
#include <tuple>
#include <utility>
#include <string>
// 1. Function argument expansion: f(args...)
template <typename... Args>
void call_print(Args... args) {
((std::cout << args << "\n"), ...);
}
// 2. Template argument expansion: Tuple<Types...>
template <typename... Ts>
using MyTuple = std::tuple<Ts...>;
// 3. Braced-init-list expansion: {args...}
template <typename... Args>
auto to_vector(Args&&... args) {
return std::vector<std::common_type_t<Args...>>{
std::forward<Args>(args)...
};
}
// 4. Using-declaration expansion: using Base::foo...
struct Base1 { void foo() { std::cout << "Base1::foo\n"; } };
struct Base2 { void foo() { std::cout << "Base2::foo\n"; } void bar() { std::cout << "Base2::bar\n"; } };
template <typename... Bases>
struct Multi : Bases... {
using Bases::foo...; // brings all foo() overloads into scope
};
// 5. Pack expansion in sizeof... (not an expansion context per se, but uses the pack)
template <typename... Ts>
constexpr std::size_t pack_size() { return sizeof...(Ts); }
int main() {
call_print(42, std::string{"hello"}, 3.14);
auto vec = to_vector(1, 2, 3, 4, 5);
for (auto x : vec) std::cout << x << " ";
std::cout << "\n";
Multi<Base1, Base2> m;
m.foo(); // ambiguous: Base1::foo or Base2::foo (as expected for using-decl pack)
m.bar(); // OK: only Base2::bar
}

Recursive Template Patterns for Pack Processing

Section titled “Recursive Template Patterns for Pack Processing”

Before C++17 fold expressions, pack processing required recursive template instantiation. The Pattern is: peel one element off the pack, process it, then recurse on the remainder [N4950 §13.7.3].

#include <iostream>
#include <type_traits>
#include <string>
// Pattern 1: Recursive print (classic head-tail recursion)
void print_recursive() {
std::cout << "\n";
}
template <typename T, typename... Rest>
void print_recursive(const T& first, const Rest&... rest) {
std::cout << first;
if constexpr (sizeof...(rest) > 0) {
std::cout << ", ";
}
print_recursive(rest...);
}
// Pattern 2: Type-level recursion: check if any type satisfies a predicate
template <template <typename> class Pred, typename... Ts>
struct any_of;
template <template <typename> class Pred>
struct any_of<Pred> : std::false_type {};
template <template <typename> class Pred, typename First, typename... Rest>
struct any_of<Pred, First, Rest...>
: std::bool_constant<Pred<First>::value || any_of<Pred, Rest...>::value> {};
template <typename T>
struct is_integral_pred : std::is_integral<T> {};
// Pattern 3: Recursive tuple for_each
template <typename Fn, typename Tuple, std::size_t... Is>
void tuple_for_each_impl(Fn&& fn, Tuple&& t, std::index_sequence<Is...>) {
(fn(std::get<Is>(std::forward<Tuple>(t))), ...);
}
template <typename Fn, typename... Ts>
void tuple_for_each(Fn&& fn, const std::tuple<Ts...>& t) {
tuple_for_each_impl(
std::forward<Fn>(fn), t,
std::index_sequence_for<Ts...>{}
);
}
int main() {
print_recursive("alpha", 42, 3.14, std::string{"zeta"});
static_assert(any_of<is_integral_pred, double, std::string, int>::value);
static_assert(!any_of<is_integral_pred, double, std::string>::value);
auto t = std::make_tuple(1, std::string{"two"}, 3.0);
tuple_for_each([](const auto& v) { std::cout << v << " "; }, t);
std::cout << "\n";
}

C++17 fold expressions replace most recursive template patterns with a single line of code. For Details, see Fold Expressions and Pack Expansion.

#include <iostream>
#include <string>
// Before (C++14 recursive):
// template <typename T>
// T sum_old(T val) { return val; }
// template <typename T, typename... Rest>
// T sum_old(T first, Rest... rest) { return first + sum_old(rest...); }
// After (C++17 fold):
template <typename... Args>
auto sum_fold(Args... args) {
return (args + ...); // unary right fold
}
// Print with separator using fold
template <typename... Args>
void print_fold(Args&&... args) {
std::string sep;
((std::cout << std::exchange(sep, ", ") << args), ...);
std::cout << "\n";
}
int main() {
static_assert(sum_fold(1, 2, 3, 4, 5) == 15);
print_fold("alpha", 42, 3.14); // alpha, 42, 3.14
}

Parameter packs are like a bag of types: Instead of writing separate template parameters for each type (T1, T2, T3), you pack them all into one bag (Types...). It’s like the difference between carrying three separate grocery bags and one big bag with everything inside. The ... syntax expands the pack — Types... becomes T1, T2, T3. You can access individual elements with std::tuple_element, iterate with fold expressions, or use sizeof...(Types) to count them.

Why it matters: Parameter packs enable variadic templates — functions and classes that accept any number of arguments. This is the foundation of std::make_unique, std::tuple, std::variant, and many other modern C++ features. Without parameter packs, you’d need to write separate overloads for each number of arguments (like the old printf approach).

The key insight: Parameter packs expand at compile time — the compiler generates separate code for each element in the pack, which is why variadic templates are zero-cost abstractions.

Overload resolution between a variadic template and a specific overload can be surprising when the Pack is empty:

#include <iostream>
// This is called for zero or more arguments
template <typename... Args>
void dispatch(Args... args) {
std::cout << "variadic: " << sizeof...(args) << " args\n";
}
// This is a better match for zero arguments IF it exists
void dispatch() {
std::cout << "no-arg overload\n";
}
int main() {
dispatch(); // calls void dispatch(), NOT the variadic
dispatch(1, 2); // calls variadic: 2 args
dispatch(42); // calls variadic: 1 arg
}

The non-variadic overload wins when the argument list matches exactly. This is by design per Overload resolution rules [N4950 §12.4.3], but it can be surprising.

A variadic template can shadow all other overloads in the same scope. The workaround is to constrain The variadic with requires or SFINAE:

#include <iostream>
#include <concepts>
struct Widget { int x; };
struct Gizmo { double y; };
// BAD: unconstrained variadic swallows everything
// template <typename... Args>
// void process(Args... args) { std::cout << "variadic\n"; }
// GOOD: constrain the variadic to avoid hijacking other overloads
template <typename T, typename... Args>
requires (sizeof...(Args) >= 2)
void process(T first, Args... rest) {
std::cout << "variadic: " << sizeof...(rest) + 1 << " args\n";
}
void process(Widget w) { std::cout << "Widget: " << w.x << "\n"; }
void process(Gizmo g) { std::cout << "Gizmo: " << g.y << "\n"; }
int main() {
process(Widget{42}); // Widget: 42
process(Gizmo{3.14}); // Gizmo: 3.14
process(1, 2, 3); // variadic: 3 args
// process(1); // ill-formed: constraint not satisfied
}

Pack expansion must appear in a valid expansion context [N4950 §13.7.3]. You cannot expand a pack in An arbitrary position:

#include <iostream>
#include <tuple>
template <typename... Ts>
void bad_expansion() {
// auto x = std::tuple<Ts...>{Ts{}...}; // ERROR: Ts{} is not a valid expansion here
// because the pack Ts... is a TYPE pack, not a value pack.
}
template <typename... Ts>
void good_expansion() {
// OK: expanding in a braced-init-list context
auto x = std::tuple<Ts...>{}; // zero-constructs each element
(void)x;
}
int main() {
good_expansion<int, double, char>();
}

When using forwarding references (Args&&...) with parameter packs, always use std::forward in The expansion. Forgetting to forward degrades rvalues to lvalues:

#include <iostream>
#include <utility>
#include <string>
// CORRECT: forwards each argument with its original value category
template <typename... Args>
void forward_correct(Args&&... args) {
some_function(std::forward<Args>(args)...);
}
// WRONG: all arguments become lvalue references
template <typename... Args>
void forward_wrong(Args&&... args) {
some_function(args...); // BUG: rvalues decay to lvalues
}
// Stub for illustration
void some_function(int&, double&&, std::string&&) {
std::cout << "called\n";
}
int main() {
int x = 10;
forward_correct(x, 3.14, std::string{"hi"}); // OK
// forward_wrong(x, 3.14, std::string{"hi"}); // would fail: double&& expects rvalue
}

This topic covers the core concepts of parameter packs and variadic templates, including underlying theory, practical implementation, and key applications.

Key concepts include:

  • core concepts and terminology
  • algorithms and computational thinking
  • practical implementation
  • security and ethical considerations
  • applications in the real world

Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.

Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.