Article by Ayman Alheraki on January 24 2026 10:07 PM
Before C++11, functions or classes that needed to accept a variable number of arguments relied on:
Repetitive overloading (10 versions of the same function)
Or C-style variadic arguments (...), which discard type safety and introduce serious runtime risks
Variadic Templates solved this by providing:
Full type safety
Compile-time expansion
Excellent performance
The foundation of modern utilities such as logging systems, formatters, factories, tuples, visitors, and wrappers
Variadic templates introduce two related concepts:
Args... → a type pack
args... → a value pack
Example:
template<typename... Args>void f(Args... args) { // Args... → types // args... → values}Call site:
f(1, 2.5, "hi");This expands to:
Args... → <int, double, const char*>
args... → (1, 2.5, "hi")
With C++17, Fold Expressions eliminate recursive templates.
template<typename... Args>void print_raw(const Args&... args) { (std::cout << ... << args) << '\n';}template<typename... Args>void print(const Args&... args) { const char* sep = ""; ((std::cout << sep << args, sep = " "), ...) << '\n';}Usage:
print(1);print(1, 2.5);print("Hello", 42, 3.14);Even if you use fold expressions, understanding recursion explains how the compiler reasons.
void print_old() { std::cout << '\n'; } // base case
template<typename T, typename... Rest>void print_old(const T& first, const Rest&... rest) { std::cout << first; if constexpr (sizeof...(rest) > 0) std::cout << ' '; print_old(rest...);}T → first argument
Rest... → remaining arguments
Each instantiation reduces the pack until the base case
✔ Educational ❌ Verbose and slower to compile than folds
sizeof...(Args)template<typename... Args>constexpr std::size_t count_types() { return sizeof...(Args);}
template<typename... Args>void count_values(const Args&... args) { std::cout << "count = " << sizeof...(args) << '\n';}template<typename... Args>void touch_all(const Args&... args) { ((std::cout << args << '\n'), ...);}std::vector from Variadic Argumentstemplate<typename... Args>auto make_vector(Args&&... args) { using T = std::common_type_t<Args...>; std::vector<T> v; v.reserve(sizeof...(Args)); (v.push_back(static_cast<T>(std::forward<Args>(args))), ...); return v;}Usage:
auto v = make_vector(1, 2, 3, 4);auto w = make_vector(1, 2.5, 3); // common_type → doubleVariadic templates are often used to forward arguments exactly as received.
Key tools:
Args&&... (forwarding references)
std::forward<Args>(args)...
template<typename T, typename... Args>std::unique_ptr<T> make(Args&&... args) { return std::make_unique<T>(std::forward<Args>(args)...);}Usage:
struct User { std::string name; int age; User(std::string n, int a) : name(std::move(n)), age(a) {}};
auto u = make<User>("Ayman", 30);Why this matters:
Preserves lvalues and rvalues
Avoids unnecessary copies
Enables clean, generic APIs
template<typename... Args>void log(const char* tag, const Args&... args) { std::cout << '[' << tag << "] "; const char* sep = ""; ((std::cout << sep << args, sep = " "), ...) << '\n';}Usage:
log("INFO", "Started", 42, 3.14);log("WARN", "Disk low:", 5, "%");This demonstrates how early tuple implementations worked conceptually.
template<typename... Ts>struct Pack;
template<>struct Pack<> {};
template<typename T, typename... Rest>struct Pack<T, Rest...> : Pack<Rest...> { T value; Pack(T v, Rest... rest) : Pack<Rest...>(rest...), value(std::move(v)) {}};Educational purpose only—modern code uses std::tuple.
std::move Instead of std::forwardtemplate<typename... Args>void bad(Args&&... args) { foo(std::move(args)...); // WRONG}Correct:
template<typename... Args>void good(Args&&... args) { foo(std::forward<Args>(args)...);}Some fold expressions fail with empty packs.
template<typename... Args>void print(const Args&... args) { if constexpr (sizeof...(Args) == 0) { std::cout << '\n'; } else { const char* sep = ""; ((std::cout << sep << args, sep = " "), ...) << '\n'; }}template<typename T>concept Streamable = requires(std::ostream& os, const T& v) { os << v;};
template<Streamable... Args>void print(const Args&... args) { const char* sep = ""; ((std::cout << sep << args, sep = " "), ...) << '\n';}Args..., args...
sizeof...(Args)
Pack expansion
Recursive templates
if constexpr
Unary and binary folds
Comma folds
Args&&...
std::forward
Factories and wrappers
Concepts and constraints
Clean diagnostics
Robust API design
template<typename F, typename... Args>decltype(auto) call_and_log(F&& f, Args&&... args) { std::cout << "call: "; const char* sep = ""; ((std::cout << sep << args, sep = ", "), ...) << '\n'; return std::invoke(std::forward<F>(f), std::forward<Args>(args)...);}template<typename F, typename... Args>void for_each_arg(F&& f, Args&&... args) { (std::invoke(std::forward<F>(f), std::forward<Args>(args)), ...);}Variadic Templates are not a luxury feature—they are a core pillar of Modern C++.
Mastering:
Pack expansion
Fold expressions
Perfect forwarding
Concepts
means you can design:
Safe, generic, high-performance APIs
Modern libraries
Professional-grade C++ systems