Variadic Templates
/ 3 min read
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 argumentsThe 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 argumentsvoid print() { std::cout << std::endl;}
// Recursive casetemplate<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:
- 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);- 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;}- 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();}- 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;}
// Usagelog(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.