Variadic Templates — Parameter Packs, Fold Expressions, Recursive Expansion
In this tutorial, you will learn about Variadic Templates. We cover key concepts, practical examples, and best practices to help you master this topic.
C++ variadic templates accept any number of template arguments through parameter packs, enabling type-safe printf-style functions, tuple-like types, and compile-time Recursion without runtime overhead.
What You'll Learn
You will write variadic function templates with parameter packs, use recursive expansion to Process individual arguments, apply fold expressions (C++17) for concise pack operations, create variadic class templates like std::tuple, and understand sizeof... for pack size queries.
Why It Matters
Before variadic templates (C++11), varargs functions like printf relied on C-style ... with no type safety. Variadic templates bring compile-time Type Checking to arbitrary-argument functions. They power std::tuple, std::make_unique, std::format, and countless libraries. Combined with C++ fold expressions, they are essential for modern generic programming.
Learning Path
graph LR
A["44: Template Specialization"] --> B["45: Variadic Templates"]
B --> C["46: SFINAE & enable_if"]
C --> D["47: constexpr & consteval"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
Basic Variadic Function Template
A variadic template uses typename... Args (a template parameter pack) and Args... args (a function parameter pack).
#include <iostream>
// Base case: single argument
template <typename T>
void print(const T& value) {
std::cout << value << "\n";
}
// Recursive variadic case: one or more arguments
template <typename T, typename... Args>
void print(const T& first, const Args&... rest) {
std::cout << first << " ";
print(rest...); // recursive call with remaining arguments
}
int main() {
print(1); // 1
print(1, 2.5, "hello", 'x'); // 1 2.5 hello x
print("only", "one", "call"); // only one call
}
The parameter pack Args captures zero or more types. rest... expands the pack, passing each argument individually to the recursive call. The base case terminates recursion when only one argument remains.
Using sizeof... with Parameter Packs
The sizeof... operator returns the number of elements in a parameter pack at compile time.
#include <iostream>
template <typename... Args>
void countAndPrint(const Args&... args) {
constexpr size_t numArgs = sizeof...(Args);
std::cout << "Number of arguments: " << numArgs << "\n";
// Print all
((std::cout << args << " "), ...);
std::cout << "\n";
}
int main() {
countAndPrint(1, 2.5, "hello");
// Number of arguments: 3
// 1 2.5 hello
countAndPrint();
// Number of arguments: 0
// (empty line)
}
Fold Expressions (C++17)
Fold expressions apply a binary operator over all elements of a parameter pack, eliminating recursion.
#include <iostream>
#include <string>
// Unary right fold (op ... pack)
template <typename... Args>
auto sum(const Args&... args) {
return (args + ...); // (arg1 + (arg2 + (arg3 + ...)))
}
// Unary left fold (... op pack)
template <typename... Args>
auto sumLeft(const Args&... args) {
return (... + args); // (((arg1 + arg2) + arg3) + ...)
}
// Binary fold (pack op ... init) — handles empty pack
template <typename... Args>
auto sumWithDefault(const Args&... args) {
return (args + ... + 0); // 0 is the identity for addition
}
// Print with fold expression (no recursion needed)
template <typename... Args>
void printFold(const Args&... args) {
((std::cout << args << " "), ...);
std::cout << "\n";
}
int main() {
std::cout << sum(1, 2, 3, 4, 5) << "\n"; // 15
std::cout << sumLeft(1, 2, 3) << "\n"; // 6
std::cout << sumWithDefault() << "\n"; // 0 (empty pack)
std::cout << sumWithDefault(1.5, 2.5) << "\n"; // 4.0
printFold("fold", "expressions", "are", "concise");
// fold expressions are concise
}
Fold operators available: +, -, *, /, %, ^, &, |, =, <, >, <<, >>, +=, -=, *=, ,, &&, ||.
Variadic Class Template
Variadic class templates power std::tuple and similar heterogeneous containers.
#include <iostream>
#include <typeinfo>
// Base case: empty tuple
template <typename... Types>
struct Tuple {
Tuple() = default;
};
// Recursive case: head + tail
template <typename Head, typename... Tail>
struct Tuple<Head, Tail...> : private Tuple<Tail...> {
Head value;
Tuple(Head h, Tail... t) : Tuple<Tail...>(t...), value(h) {}
Head& getHead() { return value; }
Tuple<Tail...>& getTail() { return *this; }
};
// Helper to get Nth element
template <size_t Index, typename... Types>
struct GetHelper;
template <typename Head, typename... Tail>
struct GetHelper<0, Head, Tail...> {
static Head& get(Tuple<Head, Tail...>& t) {
return t.getHead();
}
};
template <size_t Index, typename Head, typename... Tail>
struct GetHelper<Index, Head, Tail...> {
static auto& get(Tuple<Head, Tail...>& t) {
Tuple<Tail...>& tail = t.getTail();
return GetHelper<Index - 1, Tail...>::get(tail);
}
};
template <size_t Index, typename... Types>
auto& get(Tuple<Types...>& t) {
return GetHelper<Index, Types...>::get(t);
}
int main() {
Tuple<int, double, const char*> t(42, 3.14, "hello");
std::cout << get<0>(t) << "\n"; // 42
std::cout << get<1>(t) << "\n"; // 3.14
std::cout << get<2>(t) << "\n"; // hello
}
Variadic Template with std::index_sequence (C++14)
std::index_sequence helps expand packs into arrays or tuples.
#include <iostream>
#include <array>
#include <utility>
// Convert variadic args to array
template <typename... Args>
std::array<int, sizeof...(Args)> toArray(Args... args) {
return {static_cast<int>(args)...};
}
// Apply a function to each argument
template <typename Func, typename... Args>
void forEach(Func func, Args&&... args) {
(func(std::forward<Args>(args)), ...);
}
// Build an array with index_sequence
template <typename T, size_t... Indices>
void printIndices(const std::array<T, sizeof...(Indices)>& arr,
std::index_sequence<Indices...>) {
((std::cout << Indices << ":" << arr[Indices] << " "), ...);
std::cout << "\n";
}
template <typename T, size_t N>
void printWithIndices(const std::array<T, N>& arr) {
printIndices(arr, std::make_index_sequence<N>{});
}
int main() {
auto arr = toArray(1, 2.5, 3.7f, 'A');
for (int x : arr) std::cout << x << " ";
std::cout << "\n"; // 1 2 3 65
forEach([](auto x) { std::cout << x * 2 << " "; }, 1, 2, 3);
std::cout << "\n"; // 2 4 6
std::array<double, 3> darr = {1.1, 2.2, 3.3};
printWithIndices(darr); // 0:1.1 1:2.2 2:3.3
}
Practical: Variadic Min/Max
#include <iostream>
#include <algorithm>
template <typename T>
T minimum(T value) {
return value;
}
template <typename T, typename... Args>
T minimum(T first, Args... rest) {
T minRest = minimum(rest...);
return (first < minRest) ? first : minRest;
}
// Using fold expression (C++17)
template <typename... Args>
auto minimumFold(const Args&... args) {
return (args < ...); // < is left-associative: (((a < b) < c) < ...)
// This doesn't work for min! Use min with initializer list instead
}
// Correct fold min: use initializer list
template <typename... Args>
auto minInitList(Args... args) {
return std::min({static_cast<decltype(args)>(args)...});
}
int main() {
std::cout << minimum(5, 3, 8, 1, 9) << "\n"; // 1
std::cout << minimum("apple", "orange", "banana") << "\n"; // apple
std::cout << minInitList(5, 3, 8, 1, 9) << "\n"; // 1
}
Common Mistakes
Mistake 1: Pack expansion outside of function call context
template <typename... Args>
void wrong(Args... args) {
args...; // Error: no context for expansion
}
Packs must be expanded in a function call, initializer list, or fold expression.
Mistake 2: Forgetting base case in recursion
template <typename T, typename... Args>
void print(T first, Args... rest) {
std::cout << first;
print(rest...); // Infinite recursion when rest is empty
}
Always provide a base case (zero or one argument overload) to terminate recursion.
Mistake 3: Confusing pack types with pack values
template <typename... Args>
void func(Args... args) {
sizeof...(Args); // Number of types
sizeof...(args); // Number of arguments (same value)
}
Mistake 4: Non-portable fold expression with wrong operator
template <typename... Args>
auto sum(Args... args) {
return (args + ... + 0); // 0 is wrong for strings
}
The identity value must match the type. std::string needs ""s.
Mistake 5: Pack expansion in wrong order
((std::cout << args << " "), ...); // prints in order
(... << std::cout << args << " "); // wrong: << is left-to-right
Use the comma operator with fold expressions for correct left-to-right evaluation.
Practice Questions
- What is the output?
template <typename... Args>
auto sum(Args... args) { return (args + ... + 0); }
int main() { std::cout << sum() << " " << sum(1, 2, 3); }
Answer: 0 6
What does
sizeof...(Args)return when the pack is empty? Answer:0— it works at compile time and can be used inif constexprchecks.Write a variadic
maxfunction using fold expressions. Answer: Usestd::max({args...})— the initializer list approach works becausemaxon an initializer list is variadic.Can fold expressions work with
&&and||operators? Answer: Yes, they work with all 32 binary operators in C++, including logical operators.What is the difference between left fold and right fold? Answer: Left fold
(... + args)evaluates as(((a+b)+c)+d). Right fold(args + ...)evaluates as(a+(b+(c+d))). For associative operations like+, they produce the same result.
FAQ
Mini Project
Implement a type-safe format function using variadic templates that replaces {} placeholders with arguments:
#include <iostream>
#include <string>
// Your format function here
int main() {
std::cout << format("Hello, {}!", "World") << "\n";
// Hello, World!
std::cout << format("{} + {} = {}", 3, 4, 7) << "\n";
// 3 + 4 = 7
std::cout << format("Pi is approximately {}", 3.14159) << "\n";
// Pi is approximately 3.14159
}
This project demonstrates real-world variadic template usage. Libraries like std::format (C++20) and fmtlib use this exact mechanism internally.
What's Next
You now wield variadic templates — the foundation of compile-time argument processing. Next, you will dive into SFINAE and enable_if, which control which template overloads participate in resolution based on type properties.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro