[2026] C++ Variadic Templates | Complete Guide to Parameter Packs
이 글의 핵심
Learn C++ variadic templates: typename....Args, pack expansion, sizeof..., fold expressions, and recursive patterns—with examples for generic printf, tuples, and logging.
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 아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 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
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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 pack
- Args…args: function parameter pack
- args…: pack expansion 다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
template<typename....Args> // template parameter pack
void func(Args....args) { // function parameter pack
process(args...); // pack expansion
}
Recursive templates
다음은 cpp를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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)
다음은 cpp를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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
다음은 cpp를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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
다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 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
다음은 cpp를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 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
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 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
다음은 cpp를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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
다음은 cpp를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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)
다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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
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?