C++ Variadic Templates | Complete Guide to Parameter Packs
이 글의 핵심
A practical guide to C++ variadic templates: type-safe variadic functions and classes without C-style varargs.
What are variadic templates?
Variadic templates, introduced in C++11, let you accept any number of template arguments. You can write flexible, type-safe functions and classes.
Why use them?
- Type safety: Safer than C-style varargs (
...) - Flexibility: Handle any number of arguments
- Generality: Works across types
- Compile time: No inherent runtime cost for the dispatch pattern
// ❌ C style: not type-safe
void print(int count, ...) {
va_list args;
va_start(args, count);
// Types are unknown
va_end(args);
}
// ✅ Variadic template: type-safe
template<typename... Args>
void print(Args... args) {
(std::cout << ... << args) << '\n';
}
Basic syntax
// Variadic template function
template<typename... Args>
void print(Args... args) {
(cout << ... << args) << endl; // C++17 fold expression
}
int main() {
print(1, 2, 3); // 123
print("Hello", " ", "World"); // Hello World
}
Terminology:
typename... Args: template parameter packArgs... args: function parameter packargs...: pack expansion
template<typename... Args> // template parameter pack
void func(Args... args) { // function parameter pack
process(args...); // pack expansion
}
Recursive templates
// Base case
void print() {
cout << endl;
}
// Recursive case
template<typename T, typename... Args>
void print(T first, Args... rest) {
cout << first << " ";
print(rest...); // recurse
}
int main() {
print(1, 2, 3, 4, 5); // 1 2 3 4 5
}
The sizeof... operator
template<typename... Args>
void printCount(Args... args) {
cout << "Argument count: " << sizeof...(args) << endl;
}
int main() {
printCount(1, 2, 3); // 3
printCount("a", "b", "c", "d"); // 4
}
Fold expressions (C++17)
// Unary left fold: (... op pack)
template<typename... Args>
auto sum(Args... args) {
return (... + args); // ((arg1 + arg2) + arg3) + ...
}
// Unary right fold: (pack op ...)
template<typename... Args>
auto sum2(Args... args) {
return (args + ...); // arg1 + (arg2 + (arg3 + ...))
}
// Binary fold
template<typename... Args>
void printAll(Args... args) {
(cout << ... << args) << endl;
}
int main() {
cout << sum(1, 2, 3, 4, 5) << endl; // 15
printAll(1, " ", 2, " ", 3); // 1 2 3
}
Practical examples
Example 1: Type-safe printf
#include <iostream>
#include <sstream>
using namespace std;
void printf_impl(ostringstream& oss, const char* format) {
oss << format;
}
template<typename T, typename... Args>
void printf_impl(ostringstream& oss, const char* format, T value, Args... args) {
while (*format) {
if (*format == '%' && *(++format) != '%') {
oss << value;
printf_impl(oss, format, args...);
return;
}
oss << *format++;
}
}
template<typename... Args>
string sprintf(const char* format, Args... args) {
ostringstream oss;
printf_impl(oss, format, args...);
return oss.str();
}
int main() {
cout << sprintf("Hello % from %", "World", "C++") << endl;
cout << sprintf("% + % = %", 1, 2, 3) << endl;
}
Example 2: Tuple-like implementation
template<typename... Types>
class Tuple;
template<>
class Tuple<> {};
template<typename Head, typename... Tail>
class Tuple<Head, Tail...> : private Tuple<Tail...> {
private:
Head value;
public:
Tuple(Head h, Tail... t) : Tuple<Tail...>(t...), value(h) {}
Head& head() { return value; }
Tuple<Tail...>& tail() { return *this; }
};
template<size_t Index, typename... Types>
struct TupleElement;
template<typename Head, typename... Tail>
struct TupleElement<0, Tuple<Head, Tail...>> {
using type = Head;
static Head& get(Tuple<Head, Tail...>& t) {
return t.head();
}
};
template<size_t Index, typename Head, typename... Tail>
struct TupleElement<Index, Tuple<Head, Tail...>> {
using type = typename TupleElement<Index-1, Tuple<Tail...>>::type;
static type& get(Tuple<Head, Tail...>& t) {
return TupleElement<Index-1, Tuple<Tail...>>::get(t.tail());
}
};
template<size_t Index, typename... Types>
auto& get(Tuple<Types...>& t) {
return TupleElement<Index, Tuple<Types...>>::get(t);
}
int main() {
Tuple<int, double, string> t(42, 3.14, "Hello");
cout << get<0>(t) << endl; // 42
cout << get<1>(t) << endl; // 3.14
cout << get<2>(t) << endl; // Hello
}
Example 3: Function pipeline
template<typename... Funcs>
class Pipeline;
template<typename Func>
class Pipeline<Func> {
private:
Func func;
public:
Pipeline(Func f) : func(f) {}
template<typename T>
auto operator()(T value) {
return func(value);
}
};
template<typename Func, typename... Rest>
class Pipeline<Func, Rest...> {
private:
Func func;
Pipeline<Rest...> rest;
public:
Pipeline(Func f, Rest... r) : func(f), rest(r...) {}
template<typename T>
auto operator()(T value) {
return rest(func(value));
}
};
template<typename... Funcs>
auto makePipeline(Funcs... funcs) {
return Pipeline<Funcs...>(funcs...);
}
int main() {
auto pipeline = makePipeline(
{ return x * 2; },
{ return x + 10; },
{ return x * x; }
);
cout << pipeline(5) << endl; // ((5*2)+10)^2 = 400
}
Example 4: Variadic min/max
template<typename T>
T min(T value) {
return value;
}
template<typename T, typename... Args>
T min(T first, Args... rest) {
T restMin = min(rest...);
return first < restMin ? first : restMin;
}
// C++17 fold version
template<typename... Args>
auto minFold(Args... args) {
return (std::min)({args...});
}
int main() {
cout << min(5, 2, 8, 1, 9) << endl; // 1
cout << minFold(5, 2, 8, 1, 9) << endl; // 1
}
Pack expansion patterns
// Apply a function to each argument
template<typename Func, typename... Args>
void forEach(Func f, Args... args) {
(f(args), ...); // fold over comma
}
// Push each argument into a vector
template<typename... Args>
vector<int> makeVector(Args... args) {
vector<int> result;
(result.push_back(args), ...);
return result;
}
int main() {
forEach( { cout << x << " "; }, 1, 2, 3, 4, 5);
cout << endl;
auto v = makeVector(10, 20, 30);
for (int x : v) cout << x << " ";
}
Extracting types
// First type
template<typename... Args>
struct FirstType;
template<typename First, typename... Rest>
struct FirstType<First, Rest...> {
using type = First;
};
// Nth type
template<size_t N, typename... Args>
struct NthType;
template<typename First, typename... Rest>
struct NthType<0, First, Rest...> {
using type = First;
};
template<size_t N, typename First, typename... Rest>
struct NthType<N, First, Rest...> {
using type = typename NthType<N-1, Rest...>::type;
};
int main() {
using T1 = FirstType<int, double, string>::type; // int
using T2 = NthType<1, int, double, string>::type; // double
}
Common pitfalls
Pitfall 1: Empty packs
// ❌ Error (empty pack) for unary + fold
template<typename... Args>
auto sum(Args... args) {
return (... + args); // ill-formed if empty
}
// ✅ Provide an initializer
template<typename... Args>
auto sum(Args... args) {
return (0 + ... + args); // binary fold; empty => 0
}
Pitfall 2: Missing base case
// ❌ Infinite recursion
template<typename T, typename... Args>
void print(T first, Args... rest) {
cout << first << " ";
print(rest...); // no base case!
}
// ✅ Add a base case
void print() {} // base case
template<typename T, typename... Args>
void print(T first, Args... rest) {
cout << first << " ";
print(rest...);
}
Pitfall 3: Deduction ambiguity
// ❌ Deduction may be unclear
template<typename... Args>
void func(Args... args) {
auto result = (args + ...); // unclear if mixed types
}
// ✅ Explicit return type
template<typename... Args>
auto func(Args... args) -> decltype((args + ...)) {
return (args + ...);
}
Performance notes
// Recursive template (can increase compile time)
template<typename T, typename... Args>
T sum(T first, Args... rest) {
if constexpr (sizeof...(rest) == 0) {
return first;
} else {
return first + sum(rest...);
}
}
// Fold expression (often simpler for the compiler)
template<typename... Args>
auto sumFold(Args... args) {
return (... + args);
}
Production patterns
Pattern 1: Logging
enum class LogLevel { INFO, WARNING, ERROR };
template<typename... Args>
void log(LogLevel level, Args&&... args) {
std::ostringstream oss;
switch (level) {
case LogLevel::INFO: oss << "[INFO] "; break;
case LogLevel::WARNING: oss << "[WARN] "; break;
case LogLevel::ERROR: oss << "[ERROR] "; break;
}
(oss << ... << std::forward<Args>(args));
std::cout << oss.str() << '\n';
}
log(LogLevel::INFO, "User ", 123, " logged in");
log(LogLevel::ERROR, "Failed to connect to ", "database");
Pattern 2: Type-safe formatting (illustrative)
template<typename... Args>
std::string format(const std::string& fmt, Args&&... args) {
std::ostringstream oss;
size_t argIndex = 0;
for (size_t i = 0; i < fmt.size(); ++i) {
if (fmt[i] == '{' && i + 1 < fmt.size() && fmt[i + 1] == '}') {
((argIndex++ == 0 ? (oss << args, true) : false) || ...);
++i;
} else {
oss << fmt[i];
}
}
return oss.str();
}
Pattern 3: Event bus
template<typename... Args>
class Event {
std::vector<std::function<void(Args...)>> handlers_;
public:
void subscribe(std::function<void(Args...)> handler) {
handlers_.push_back(std::move(handler));
}
void emit(Args... args) {
for (auto& handler : handlers_) {
handler(args...);
}
}
};
Event<int, std::string> userEvent;
userEvent.subscribe( {
std::cout << "User " << id << ": " << name << '\n';
});
userEvent.emit(123, "Alice");
FAQ
Q1: When are variadic templates the right tool?
A: For APIs with a variable number of arguments (printf-style helpers), generic factories, wrappers, and metaprogramming over type lists.
Q2: Recursion vs fold expressions?
A: On C++17 and later, fold expressions are usually more concise and compile faster than hand-written recursion for the same logic.
Q3: Variadic templates vs varargs functions?
A: Prefer templates for type safety and compile-time checking; avoid C-style ... varargs in new code.
Q4: Is there runtime overhead?
A: No for the expansion itself: work is done at compile time, and the generated code is typically equivalent to manually written repetitions.
Q5: How do I debug template errors?
A: Use static_assert, read instantiation notes carefully, and test small cases first.
Q6: What is sizeof...?
A: It yields the number of arguments in a pack; it is a compile-time constant.
Q7: How do I handle an empty pack?
A: Use a binary fold with an initializer, e.g. (0 + ... + args), or branch with if constexpr (sizeof...(args) == 0).
Q8: Further reading?
A: C++ Templates: The Complete Guide (Vandevoorde, Josuttis, Gregor); cppreference — Parameter pack; Compiler Explorer.
Related: Variadic templates (advanced), fold expressions, perfect forwarding.
In one sentence: Variadic templates let you write type-safe APIs that accept any number of arguments, with all checking at compile time.
Related posts
- C++ Fold Expressions
- C++ Template basics
- C++ Fold Expressions (EN)
Practical tips
Debugging
- Enable and fix compiler warnings first.
- Reproduce issues with minimal examples.
Performance
- Do not optimize without profiling.
- Define measurable goals before tuning.
Code review
- Watch for common review feedback in your team.
- Follow your team’s coding conventions.
Checklist
Before coding
- Is this the best fit for the problem?
- Will teammates understand and maintain it?
- Does it meet performance requirements?
While coding
- Are warnings cleared?
- Are edge cases handled?
- Is error handling appropriate?
At review
- Is intent clear?
- Are tests sufficient?
- Is documentation adequate?
Keywords
C++, variadic templates, parameter pack, fold expression, templates, TMP.
See also
- C++ Fold Expressions
- C++ Template basics
- C++
autodeduction - C++ CTAD
- C++20 Concepts