Variadic Templates
Table of Contents
- The Problem: Variable Number of Arguments
- Solution 1: Manual Function Overloading
- Solution 2: Using std::vector
- Variadic Templates
- Parameter Packs Deep Dive
- Modern C++17: Fold Expressions
- Common Variadic Patterns
- Summary
Variadic Templates: From Problem to Solution
The Problem: Variable Number of Arguments
Suppose we want to write a min function that finds the minimum of any number of values:
The following example is taken from the Concepts chapter.
Here in this example Comparable is a concept, so dont get confused.
template <Comparable T>
T min(const T& a, const T& b) {
return a < b ? a : b;
}
min(2.4, 7.5); // ✓ This works
min(2.4, 7.5, 5.3); // ✗ ERROR: No matching function
min(2.4, 7.5, 5.3, 1.2); // ✗ ERROR: No matching function
How do we make min accept a variable number of parameters?
Solution 1: Function Overloading (The Manual Way)
We could manually write overloads for different numbers of parameters:
// 2 parameters
template <Comparable T>
T min(const T& a, const T& b) {
return a < b ? a : b;
}
// 3 parameters
template <Comparable T>
T min(const T& a, const T& b, const T& c) {
auto m = min(b, c); // Calls 2-parameter version
return a < m ? a : m;
}
// 4 parameters
template <Comparable T>
T min(const T& a, const T& b, const T& c, const T& d) {
auto m = min(b, c, d); // Calls 3-parameter version
return a < m ? a : m;
}
Results:
min(2.4, 7.5); // ✓ Works
min(2.4, 7.5, 5.3); // ✓ Works now
min(2.4, 7.5, 5.3, 1.2); // ✓ Works too!
min(2.4, 7.5, 5.3, 1.2, 3.4, 6.7, 8.9, 9.1); // ✗ Need to write more overloads...
Problems with This Approach
Tedious: Need to write many overloads manually
Limited: Only works up to the number of overloads you write
Not scalable: What if someone needs 10 or 20 parameters?
Repetitive: Notice the pattern? The compiler should handle this!
Solution 2: Using std::vector (The Dynamic Way)
Can we use a vector to hold variable numbers of arguments?
template <Comparable T>
T min(const std::vector<T>& values) {
if (values.size() == 1) return values[0];
const auto& first = values[0];
std::vector<T> rest(++values.begin(), values.end());
auto m = min(rest); // Recursive call
return first < m ? first : m;
}
// Usage with brace initialization
min({2.4, 7.5});
min({2.4, 7.5, 5.3});
min({2.4, 7.5, 5.3, 1.2});
Problems with This Approach
Runtime overhead: Must allocate a vector for every call
Recursive copying: Each recursive call copies the remaining elements
Memory allocation: Dynamic memory allocation is expensive
Awkward syntax: Requires braces {} around arguments
No compile-time optimization: Cannot be fully optimized away
Key insight: We need the compiler to generate the code at compile-time, not handle it at runtime!
So how can we ask compiler to generate these underlying functions for us ? Here comes the Variadic Templates.
Variadic Templates
Enter C++11: A Game-Changing Feature
C++11 introduced variadic templates, a powerful feature that allows templates to accept a variable number of arguments. Instead of manually writing overloads or relying on runtime containers, variadic templates let the compiler automatically generate all the code we need at compile-time!
What is a Variadic Template?
A variadic template is a template that can accept any number (zero or more) of template arguments. It uses a special construct called a parameter pack to capture these arguments.
Definition:
A variadic template uses parameter packs (
...) to accept and work with a variable number of types or values, enabling type-safe, compile-time generation of code for any number of arguments.
Basic Syntax
// Template with parameter pack
template <typename... Args>
// ^^^^^^
// Parameter pack (captures 0 or more types)
void function(Args... args) {
// ^^^^^^^^^^^^
// Function parameter pack (captures 0 or more values)
// Use args... here
}
Key syntax elements:
typename... Args- Declares a template parameter pack (types)Args... args- Declares a function parameter pack (values)args...- Expands the parameter pack
The Complete Solution
// Base case: single value (stops recursion)
template <Comparable T>
T min(const T& value) {
return value;
}
// Recursive case: 2 or more values (variadic template)
template <Comparable T, Comparable... Args>
// ^^^^^^^^^^^^^^^^^^
// Parameter pack: accepts 0+ types
T min(const T& first, const Args&... rest) {
// ^^^^^^^^^^^^^^^^^
// Function parameter pack: 0+ arguments
auto min_rest = min(rest...); // Recursive call with remaining args
// ^^^^^^^ // Pack expansion: expands rest...
return first < min_rest ? first : min_rest;
}
Usage:
min(2.4, 7.5); // ✓ Works!
min(2.4, 7.5, 5.3); // ✓ Works!
min(2.4, 7.5, 5.3, 1.2); // ✓ Works!
min(2.4, 7.5, 5.3, 1.2, 3.4); // ✓ Works!
// Works with ANY number of arguments!
How It Works
Let’s trace min(5, 2, 8, 1) and see what template functions the compiler generates:
═══════════════════════════════════════════════════════════════════════════════
COMPILER GENERATES THESE FUNCTIONS FOR US!
═══════════════════════════════════════════════════════════════════════════════
╔═══════════════════════════════════════════════════════════════════════════╗
║ CALL 1: min(5, 2, 8, 1) ║
║ ║
║ Template Deduction: T = int, Args = [int, int, int] ║
║ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ║
║ ┃ GENERATED FUNCTION: ┃ ║
║ ┃ int min(const int& v, const int& a0, const int& a1, const int& a2) {┃ ║
║ ┃ auto m = min(a0, a1, a2); // Calls next instantiation ┃ ║
║ ┃ return v < m ? v : m; ┃ ║
║ ┃ } ┃ ║
║ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ║
║ ║
║ Runtime Values: v=5, a0=2, a1=8, a2=1 ║
║ Calls: min(2, 8, 1) ────────────────────────────────────────────────────→║
╚═══════════════════════════════════════════════════════════════════════════╝
↓
╔═══════════════════════════════════════════════════════════════════════════╗
║ CALL 2: min(2, 8, 1) ║
║ ║
║ Template Deduction: T = int, Args = [int, int] ║
║ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ║
║ ┃ GENERATED FUNCTION: ┃ ║
║ ┃ int min(const int& v, const int& a0, const int& a1) { ┃ ║
║ ┃ auto m = min(a0, a1); // Calls next instantiation ┃ ║
║ ┃ return v < m ? v : m; ┃ ║
║ ┃ } ┃ ║
║ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ║
║ ║
║ Runtime Values: v=2, a0=8, a1=1 ║
║ Calls: min(8, 1) ──────────────────────────────────────────────────────→ ║
╚═══════════════════════════════════════════════════════════════════════════╝
↓
╔═══════════════════════════════════════════════════════════════════════════╗
║ CALL 3: min(8, 1) ║
║ ║
║ Template Deduction: T = int, Args = [int] ║
║ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ║
║ ┃ GENERATED FUNCTION: ┃ ║
║ ┃ int min(const int& v, const int& a0) { ┃ ║
║ ┃ auto m = min(a0); // Calls base case ┃ ║
║ ┃ return v < m ? v : m; ┃ ║
║ ┃ } ┃ ║
║ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ║
║ ║
║ Runtime Values: v=8, a0=1 ║
║ Calls: min(1) ─────────────────────────────────────────────────────────→ ║
╚═══════════════════════════════════════════════════════════════════════════╝
↓
╔═══════════════════════════════════════════════════════════════════════════╗
║ CALL 4: min(1) ★ BASE CASE ★ ║
║ ║
║ Template Deduction: T = int, Args = [] (empty!) ║
║ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ║
║ ┃ MATCHED BASE CASE FUNCTION: ┃ ║
║ ┃ int min(const int& v) { ┃ ║
║ ┃ return v; // No recursion! ┃ ║
║ ┃ } ┃ ║
║ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ║
║ ║
║ Runtime Values: v=1 ║
║ Returns: 1 ← Recursion stops! ║
╚═══════════════════════════════════════════════════════════════════════════╝
═══════════════════════════════════════════════════════════════════════════════
RETURN VALUES (Unwinding)
═══════════════════════════════════════════════════════════════════════════════
min(1) returns → 1
↑
┌───────────────────────────────────────────────────────────────────┐
│ min(8, 1): m=1, return 8 < 1 ? 8 : 1 → returns 1 │
└───────────────────────────────────────────────────────────────────┘
↑
┌───────────────────────────────────────────────────────────────────┐
│ min(2, 8, 1): m=1, return 2 < 1 ? 2 : 1 → returns 1 │
└───────────────────────────────────────────────────────────────────┘
↑
┌───────────────────────────────────────────────────────────────────┐
│ min(5, 2, 8, 1): m=1, return 5 < 1 ? 5 : 1 → returns 1 │
└───────────────────────────────────────────────────────────────────┘
═══════════════════════════════════════════════════════════════════════════════
FINAL RESULT: 1 ✓
═══════════════════════════════════════════════════════════════════════════════
💡 KEY INSIGHT: The compiler generated 4 complete functions for us!
✓ min(int, int, int, int) - 4 parameters
✓ min(int, int, int) - 3 parameters
✓ min(int, int) - 2 parameters
✓ min(int) - 1 parameter (base case)
All at COMPILE TIME with ZERO runtime overhead!
═══════════════════════════════════════════════════════════════════════════════
Understanding the Syntax: Parameter Packs Deep Dive
What is a Parameter Pack?
A parameter pack is a template parameter that accepts zero or more template arguments. Think of it as a compile-time container that holds a variable number of elements (types or values).
There are three types of parameter packs:
- Template Parameter Pack - holds types
- Function Parameter Pack - holds function arguments (values)
- Template Template Parameter Pack - holds template templates
1. Template Parameter Pack Declaration
template <Comparable T, Comparable... Args>
// ^^^^^^^^^^^^^^^^^
// Template parameter pack
Syntax breakdown:
...Argsdeclares a template parameter pack namedArgs- The
...comes before the identifier when capturing - It can match zero or more types
- Each type must satisfy the
Comparableconcept
Examples:
// Type parameter pack
template<typename... Types>
struct Container {};
Container<int, double, char> c1; // Types = [int, double, char]
Container<std::string> c2; // Types = [std::string]
Container<> c3; // Types = [] (empty!)
// Non-type parameter pack
template<int... Values>
struct IntList {};
IntList<1, 2, 3, 4> list1; // Values = [1, 2, 3, 4]
IntList<42> list2; // Values = [42]
IntList<> list3; // Values = [] (empty!)
// Template template parameter pack
template<template<typename> typename... Templates>
struct TemplateList {};
TemplateList<std::vector, std::list, std::deque> tl;
2. Function Parameter Pack Declaration
T min(const T& first, const Args&... rest)
// ^^^^^^^^^^^^^^^^^
// Function parameter pack
Syntax breakdown:
const Args&... restdeclares a function parameter pack namedrest- The
...comes before the identifier when capturing Argsis expanded first (it’s a template parameter pack)- Then
&...creates references to each expanded type - Finally,
restnames the entire pack
What this expands to:
If Args = [int, double, char], then:
const Args&... rest
↓
const int& r0, const double& r1, const char& r2
More examples:
// By value
void func1(Types... args); // Takes copies
// Expands to: Type1 arg1, Type2 arg2, ...
// By const reference
void func2(const Types&... args); // Takes const refs
// Expands to: const Type1& arg1, const Type2& arg2, ...
// By forwarding reference (perfect forwarding)
void func3(Types&&... args); // Universal references
// Expands to: Type1&& arg1, Type2&& arg2, ...
3. Pack Expansion: The Magic Happens
Pack expansion is where the compiler replaces the pattern with actual elements. The ... comes after the pattern when expanding.
auto min_rest = min(rest...);
// ^^^^^^^^
// Pack expansion
How it works:
If rest contains [a, b, c], then:
min(rest...)
↓
min(a, b, c)
Pack Expansion Contexts
Parameter packs can be expanded in many contexts:
A. Function Call Arguments
template<typename... Args>
void forward_to_func(Args&&... args) {
some_function(std::forward<Args>(args)...);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Pattern: std::forward<Args>(args)
// Expands to: std::forward<Arg1>(arg1), std::forward<Arg2>(arg2), ...
}
B. Initializer Lists
template<typename... Args>
void example(Args... args) {
std::vector<int> vec{args...}; // Expands to: {arg1, arg2, arg3, ...}
auto tuple = std::tuple{args...}; // Expands to: tuple{arg1, arg2, arg3, ...}
int array[] = {args...}; // Expands to: {arg1, arg2, arg3, ...}
}
C. Template Arguments
template<typename... Args>
void example(Args... args) {
// Pattern: decltype(args)
std::tuple<decltype(args)...> tpl;
// Expands to: std::tuple<decltype(arg1), decltype(arg2), ...>
// Pattern: std::vector<Args>
std::tuple<std::vector<Args>...> vec_tuple;
// Expands to: std::tuple<std::vector<Arg1>, std::vector<Arg2>, ...>
}
D. Base Class Lists
template<typename... Bases>
struct MultiInherit : Bases... {
// ^^^^^^^^^
// Expands to: Base1, Base2, Base3, ...
using Bases::foo...; // Bring all foo() methods into scope
// ^^^^^
// Expands to: using Base1::foo; using Base2::foo; ...
};
E. Lambda Captures
template<typename... Args>
void example(Args... args) {
// Capture by copy
auto lambda1 = [args...] { return process(args...); };
// ^^^^^^^^ ^^^^^^^^
// Capture pack Expand pack
// Capture by reference
auto lambda2 = [&args...] { return process(args...); };
// Init-capture with move (C++20)
auto lambda3 = [...args = std::move(args)] {
return process(args...);
};
}
Complex Pack Expansion Patterns
The pattern can be arbitrarily complex:
template<typename... Args>
void complex_example(Args... args) {
// Simple pattern: just the pack
func(args...);
// Expands to: func(arg1, arg2, arg3, ...)
// Pattern with function call
func(transform(args)...);
// Expands to: func(transform(arg1), transform(arg2), ...)
// Pattern with template
func(std::make_unique<Args>(args)...);
// Expands to: func(std::make_unique<Arg1>(arg1),
// std::make_unique<Arg2>(arg2), ...)
// Pattern with multiple operations
func((args * 2 + 1)...);
// Expands to: func((arg1 * 2 + 1), (arg2 * 2 + 1), ...)
}
Multiple Parameter Packs in One Expression
When expanding multiple packs simultaneously, they must have the same length:
template<typename... Ts, typename... Us>
void zip(Ts... ts, Us... us) {
// Both packs must have same length
auto pairs = std::tuple{std::pair(ts, us)...};
// Expands to: std::tuple{std::pair(t1, u1), std::pair(t2, u2), ...}
}
zip(1, 2, 3, "a", "b", "c"); // OK: both have 3 elements
zip(1, 2, "a"); // ERROR: first has 2, second has 1
sizeof…() Operator
Get the number of elements in a parameter pack:
template<typename... Args>
void example(Args... args) {
constexpr size_t type_count = sizeof...(Args); // Number of types
constexpr size_t arg_count = sizeof...(args); // Number of arguments
static_assert(sizeof...(Args) == sizeof...(args)); // Always true
std::cout << "Received " << sizeof...(args) << " arguments\n";
}
example(1, 2.5, "hello"); // Prints: Received 3 arguments
Nested Pack Expansion
Packs can be expanded inside other packs:
template<typename... Outer>
void nested(Outer... outer) {
// Inner pack expansion inside outer pack expansion
auto result = std::tuple{
std::vector{outer, outer, outer}...
// For each outer element, create a vector with 3 copies
};
// If outer = [1, 2, 3], creates:
// std::tuple{std::vector{1, 1, 1},
// std::vector{2, 2, 2},
// std::vector{3, 3, 3}}
}
Pack Expansion in sizeof
template<typename... Args>
void example(Args... args) {
// Get total size of all arguments
size_t total_size = (sizeof(args) + ...); // Fold expression
// Or create array of sizes
size_t sizes[] = {sizeof(args)...};
// Expands to: {sizeof(arg1), sizeof(arg2), sizeof(arg3), ...}
}
Key Rules for Pack Expansion
-
Ellipsis position matters:
...Name= capture a packPattern...= expand a pack
-
Expansion must be in valid context (see contexts above)
-
Cannot expand outside valid context:
// ERROR: Can't expand in arbitrary expression template<typename... Args> void bad(Args... args) { int x = args...; // ERROR! Not a valid expansion context } -
Multiple packs in one expansion must have same length
-
Empty packs are valid:
template<typename... Args> void func(Args... args) {} func(); // OK! Args and args are both empty
Visual Summary: Capture vs Expansion
template<typename... Args> // Capture template pack
void example(Args... args) { // Capture function pack
// ^^^^^^^^^^^^
// This is expansion! Args... becomes Arg1 arg1, Arg2 arg2, ...
func(args...); // Expand pack
// ^^^^^^^
// Pattern = args, Expansion = args...
func((args + 1)...); // Expand with pattern
// ^^^^^^^^^^^^^
// Pattern = (args + 1), Expansion = (args + 1)...
}
Remember:
- Ellipsis before = Capture (
...name) - Ellipsis after = Expand (
pattern...)
The Pattern: Base Case + Recursive Case
Variadic templates typically follow this pattern:
// BASE CASE: Handles the "stop condition"
template <typename T>
ReturnType function(T value) {
// Handle single value
return /* something */;
}
// RECURSIVE CASE: Handles 2+ arguments
template <typename T, typename... Args>
ReturnType function(T first, Args... rest) {
auto result = function(rest...); // Recurse with remaining args
// Combine first with result
return /* combined result */;
}
More Examples
Example 1: Print All Arguments
// Base case
void print() {
std::cout << std::endl;
}
// Recursive case
template <typename T, typename... Args>
void print(const T& first, const Args&... rest) {
std::cout << first << " ";
print(rest...); // Recursive call
}
// Usage
print(1, 2.5, "hello", 'x');
// Output: 1 2.5 hello x
Example 2: Sum All Arguments
// Base case
template <typename T>
T sum(T value) {
return value;
}
// Recursive case
template <typename T, typename... Args>
T sum(T first, Args... rest) {
return first + sum(rest...);
}
// Usage
auto result = sum(1, 2, 3, 4, 5); // result = 15
Example 3: Check All Conditions
// Base case
bool all_true(bool value) {
return value;
}
// Recursive case
template <typename... Args>
bool all_true(bool first, Args... rest) {
return first && all_true(rest...);
}
// Usage
bool result = all_true(true, true, false, true); // result = false
Modern C++17 Alternative: Fold Expressions
C++17 introduced fold expressions, which provide a more concise syntax:
// Using fold expression (C++17)
template <Comparable T, Comparable... Args>
T min(T first, Args... rest) {
return (first < ... < rest) ? first : min(rest...);
}
// Even simpler with fold
template <typename... Args>
void print(const Args&... args) {
((std::cout << args << " "), ...); // Fold over comma operator
std::cout << std::endl;
}
template <typename... Args>
auto sum(Args... args) {
return (... + args); // Fold over + operator
}
template <typename... Args>
bool all_true(Args... args) {
return (... && args); // Fold over && operator
}
Fold expression will be covered in a separate section in more details.
Comparison: All Three Approaches
| Approach | Code Size | Runtime Cost | Flexibility | Syntax |
|---|---|---|---|---|
| Manual Overloading | Large (N functions for N args) | None | Limited | Simple |
| Vector | Small | High (allocation, copying) | Unlimited | Awkward braces |
| Variadic Templates | Generated at compile-time | None (fully inlined) | Unlimited | Clean |
Key Advantages of Variadic Templates
Zero runtime overhead: All code generated at compile-time
Type-safe: Compiler checks all types
Unlimited flexibility: Works with any number of arguments
Clean syntax: No braces or wrappers needed
Fully optimizable: Compiler can inline everything
Compile-time errors: Problems caught during compilation
Common Patterns
Pattern 1: Process First, Recurse on Rest
template <typename T>
void process_all(T value) {
process(value); // Base case
}
template <typename T, typename... Args>
void process_all(T first, Args... rest) {
process(first); // Process first
process_all(rest...); // Recurse on rest
}
Pattern 2: Accumulate Result
template <typename T>
T accumulate(T value) {
return value; // Base case
}
template <typename T, typename... Args>
T accumulate(T first, Args... rest) {
return combine(first, accumulate(rest...)); // Combine with result
}
Pattern 3: Check All Elements
template <typename T>
bool check_all(T value) {
return check(value); // Base case
}
template <typename T, typename... Args>
bool check_all(T first, Args... rest) {
return check(first) && check_all(rest...); // Short-circuit on false
}
Important Notes
1. Base Case is Essential
Without a base case, recursion never stops:
// WRONG: No base case!
template <typename T, typename... Args>
void print(T first, Args... rest) {
std::cout << first << " ";
print(rest...); // Infinite recursion when rest is empty!
}
2. Parameter Packs Must Be Last
// CORRECT
template <typename First, typename... Rest>
void func(First f, Rest... r);
// WRONG: Can't have parameters after pack
template <typename... Args, typename Last> // ERROR!
void func(Args... args, Last l);
3. Empty Packs Are Valid
template <typename... Args>
void func(Args... args) {
std::cout << "Number of args: " << sizeof...(args) << "\n";
}
func(); // Valid! sizeof...(args) = 0
func(1); // sizeof...(args) = 1
func(1, 2, 3); // sizeof...(args) = 3
Summary
Variadic templates solve the problem of writing functions that accept a variable number of arguments:
- Syntax:
template <typename... Args>for parameter packs - Pattern: Base case + recursive case
- Expansion:
args...expands the pack - Benefits: Zero runtime cost, type-safe, unlimited flexibility
- Modern C++17: Fold expressions provide even more concise syntax
Variadic templates are a powerful tool that combines:
- The flexibility of runtime solutions (like vectors)
- The performance of compile-time code generation
- The elegance of recursive algorithms
They’re essential for modern C++ generic programming!