skip to content
Mehdi Mehdikhani
Table of Contents

Variadic templates let you write functions and classes that accept any number of arguments. They’re the foundation for many C++11 features like std::make_unique and std::tuple.

What Variadic Templates Actually Are

The ... syntax lets you define templates that accept a variable number of parameters:

template<typename... Args>
void print(Args... args) {
// args is a parameter pack containing all the arguments
}
print(1, "hello", 3.14, 'c'); // Any number and type of arguments

The typename... Args is a template parameter pack, and Args... args is a function parameter pack.

How to Unpack Parameter Packs

Before C++17, you needed recursion to process parameter packs:

// Base case - no more arguments
void print() {
std::cout << std::endl;
}
// Recursive case
template<typename T, typename... Args>
void print(T first, Args... rest) {
std::cout << first << " ";
print(rest...); // Recursive call with remaining arguments
}

C++17 added fold expressions, making it much simpler:

template<typename... Args>
void print(Args... args) {
((std::cout << args << " "), ...); // Fold expression
std::cout << std::endl;
}

When I Actually Use Variadic Templates

Most of the time, I use variadic templates for:

  1. Factory functions: Creating objects with any constructor arguments:
template<typename T, typename... Args>
std::unique_ptr<T> make_object(Args&&... args) {
return std::make_unique<T>(std::forward<Args>(args)...);
}
auto obj = make_object<MyClass>(arg1, arg2, arg3);
  1. Wrapper functions: Functions that forward arguments to other functions:
template<typename F, typename... Args>
auto call_with_timing(F&& func, Args&&... args) {
auto start = std::chrono::steady_clock::now();
auto result = func(std::forward<Args>(args)...);
auto duration = std::chrono::steady_clock::now() - start;
std::cout << "Function took: " << duration.count() << "ms" << std::endl;
return result;
}
  1. Type-safe printf: Building format functions that work with any types:
template<typename... Args>
void safe_printf(const std::string& format, Args... args) {
std::ostringstream oss;
safe_printf_impl(oss, format, args...);
std::cout << oss.str();
}
  1. Container constructors: Building containers from multiple values:
template<typename T, typename... Args>
class MyContainer {
std::vector<T> data_;
public:
MyContainer(Args... args) : data_{args...} {}
};
MyContainer<int> container(1, 2, 3, 4, 5);

Counting Arguments

You can get the number of arguments at compile time:

template<typename... Args>
void process(Args... args) {
constexpr size_t count = sizeof...(Args);
std::cout << "Processing " << count << " arguments" << std::endl;
}

Real World Example

Here’s a practical logging function I built:

enum class LogLevel { Debug, Info, Warning, Error };
template<typename... Args>
void log(LogLevel level, const std::string& format, Args... args) {
std::ostringstream oss;
format_impl(oss, format, args...);
std::string prefix;
switch (level) {
case LogLevel::Debug: prefix = "[DEBUG] "; break;
case LogLevel::Info: prefix = "[INFO] "; break;
case LogLevel::Warning: prefix = "[WARN] "; break;
case LogLevel::Error: prefix = "[ERROR] "; break;
}
std::cout << prefix << oss.str() << std::endl;
}
// Usage
log(LogLevel::Info, "User {} logged in at {}", username, timestamp);
log(LogLevel::Error, "Failed to open file: {}", filename);

The Pattern I Follow

For simple cases, I use fold expressions (C++17):

template<typename... Args>
auto sum(Args... args) {
return (args + ...); // Fold expression
}

For complex processing, I still use recursion:

template<typename T>
void process_each(T&& item) {
// Process single item
}
template<typename T, typename... Args>
void process_each(T&& first, Args&&... rest) {
process_item(std::forward<T>(first));
process_each(std::forward<Args>(rest)...);
}

Variadic Class Templates

You can also use variadic templates with classes:

template<typename... Types>
class TypeList {
public:
static constexpr size_t size = sizeof...(Types);
};
template<typename... Args>
class Tuple {
// Implementation details...
};
Tuple<int, std::string, double> my_tuple;

Variadic templates are powerful once you understand the syntax. They enable generic programming patterns that weren’t possible before C++11.