SimplifyC++ Article
Variadic Templates in C++: From Zero to Hero
Variadic Templates in C++: From Zero to Hero
How to Write Templates That Accept Any Number of Types and Build High-Performance, Zero-Overhead Utilities
Why This Topic Matters
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
1. The Core Idea: Type Packs and Value Packs
Variadic templates introduce two related concepts:
Args...→ a type packargs...→ 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")
2. A Real “Hello World”: Printing Any Number of Values
With C++17, Fold Expressions eliminate recursive templates.
Printing Without Separators
template<typename... Args>void print_raw(const Args&... args) { (std::cout << ... << args) << '\n';}Printing With Spaces (No Trailing Space)
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);3. Pre-C++17: Recursive Variadic Templates (You Must Understand This)
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 argumentRest...→ remaining argumentsEach instantiation reduces the pack until the base case
✔ Educational ❌ Verbose and slower to compile than folds
4. Counting Elements: 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';}5. Pack Expansion: The Most Important Skill
Applying an Operation to Each Argument
template<typename... Args>void touch_all(const Args&... args) { ((std::cout << args << '\n'), ...);}Building a std::vector from Variadic Arguments
template<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 → double6. Perfect Forwarding: The Professional Weapon
Variadic templates are often used to forward arguments exactly as received.
Key tools:
Args&&...(forwarding references)std::forward<Args>(args)...
Generic Factory Example
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
7. A Practical Example: A Real Logger
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, "%");8. Variadic Templates in Classes (Tuple-Like Concept)
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.
9. Common and Dangerous Mistakes
1) Using std::move Instead of std::forward
template<typename... Args>void bad(Args&&... args) { foo(std::move(args)...); // WRONG}Correct:
template<typename... Args>void good(Args&&... args) { foo(std::forward<Args>(args)...);}2) Assuming Folds Accept Empty Packs
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'; }}3) Ignoring Constraints (Concepts – C++20)
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';}10. Zero-to-Hero Mental Roadmap
Level 1 — Fundamentals
Args...,args...sizeof...(Args)Pack expansion
Level 2 — Execution
Recursive templates
if constexpr
Level 3 — C++17 Fold Expressions
Unary and binary folds
Comma folds
Level 4 — Perfect Forwarding
Args&&...std::forwardFactories and wrappers
Level 5 — Professional Interfaces
Concepts and constraints
Clean diagnostics
Robust API design
Selected Advanced Utilities
1) Call Wrapper with Argument Logging
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)...);}2) Apply a Function to All Arguments
template<typename F, typename... Args>void for_each_arg(F&& f, Args&&... args) { (std::invoke(std::forward<F>(f), std::forward<Args>(args)), ...);}Final Summary
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