C++ 가변 인자 템플릿 | "가변 템플릿" 완벽 가이드
이 글의 핵심
C++ 가변 인자 템플릿에 대한 실전 가이드입니다.
가변 인자 템플릿이란?
가변 인자 템플릿 (Variadic Templates) 은 C++11에서 도입된 기능으로, 임의 개수의 템플릿 인자를 받을 수 있습니다. 타입 안전하면서도 유연한 함수와 클래스를 작성할 수 있습니다.
왜 필요한가?:
- 타입 안전: C 스타일 가변 인자(
...)보다 안전 - 유연성: 임의 개수의 인자 처리
- 일반성: 모든 타입에 대해 동작
- 컴파일 타임: 런타임 오버헤드 없음
// ❌ C 스타일: 타입 불안전
void print(int count, ...) {
va_list args;
va_start(args, count);
// 타입을 알 수 없음
va_end(args);
}
// ✅ 가변 인자 템플릿: 타입 안전
template<typename... Args>
void print(Args... args) {
(std::cout << ... << args) << '\n';
}
기본 문법
// 가변 인자 템플릿 함수
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
}
용어:
typename... Args: 템플릿 파라미터 팩 (Template Parameter Pack)Args... args: 함수 파라미터 팩 (Function Parameter Pack)args...: 팩 확장 (Pack Expansion)
template<typename... Args> // 템플릿 파라미터 팩
void func(Args... args) { // 함수 파라미터 팩
process(args...); // 팩 확장
}
재귀 템플릿
// 종료 조건
void print() {
cout << endl;
}
// 재귀 케이스
template<typename T, typename... Args>
void print(T first, Args... rest) {
cout << first << " ";
print(rest...); // 재귀 호출
}
int main() {
print(1, 2, 3, 4, 5); // 1 2 3 4 5
}
sizeof… 연산자
template<typename... Args>
void printCount(Args... args) {
cout << "인자 개수: " << sizeof...(args) << endl;
}
int main() {
printCount(1, 2, 3); // 3
printCount("a", "b", "c", "d"); // 4
}
Fold Expression (C++17)
// 단항 왼쪽 fold: (... op pack)
template<typename... Args>
auto sum(Args... args) {
return (... + args); // ((arg1 + arg2) + arg3) + ...
}
// 단항 오른쪽 fold: (pack op ...)
template<typename... Args>
auto sum2(Args... args) {
return (args + ...); // arg1 + (arg2 + (arg3 + ...))
}
// 이항 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
}
실전 예시
예시 1: 제네릭 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;
}
예시 2: 튜플 구현
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
}
예시 3: 함수 체이닝
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
}
예시 4: 가변 인자 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 expression 버전
template<typename... Args>
auto minFold(Args... args) {
return (args < ...); // 오른쪽 fold
}
int main() {
cout << min(5, 2, 8, 1, 9) << endl; // 1
cout << minFold(5, 2, 8, 1, 9) << endl; // 1
}
파라미터 팩 확장
// 각 인자에 함수 적용
template<typename Func, typename... Args>
void forEach(Func f, Args... args) {
(f(args), ...); // fold expression
}
// 각 인자를 벡터에 추가
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 << " ";
}
타입 추출
// 첫 번째 타입 추출
template<typename... Args>
struct FirstType;
template<typename First, typename... Rest>
struct FirstType<First, Rest...> {
using type = First;
};
// N번째 타입 추출
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
}
자주 발생하는 문제
문제 1: 빈 팩 처리
// ❌ 에러 (빈 팩)
template<typename... Args>
auto sum(Args... args) {
return (... + args); // 빈 팩이면 에러
}
// ✅ 초기값 제공
template<typename... Args>
auto sum(Args... args) {
return (0 + ... + args); // 이항 fold
}
문제 2: 재귀 종료 조건 누락
// ❌ 무한 재귀
template<typename T, typename... Args>
void print(T first, Args... rest) {
cout << first << " ";
print(rest...); // 종료 조건 없음!
}
// ✅ 종료 조건 추가
void print() {} // 종료 조건
template<typename T, typename... Args>
void print(T first, Args... rest) {
cout << first << " ";
print(rest...);
}
문제 3: 타입 추론 실패
// ❌ 타입 추론 실패
template<typename... Args>
void func(Args... args) {
auto result = (args + ...); // 타입 불명확
}
// ✅ 명시적 반환 타입
template<typename... Args>
auto func(Args... args) -> decltype((args + ...)) {
return (args + ...);
}
성능 고려사항
// 재귀 템플릿 (컴파일 시간 증가)
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 (더 빠름)
template<typename... Args>
auto sumFold(Args... args) {
return (... + args);
}
실무 패턴
패턴 1: 로깅 시스템
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");
패턴 2: 타입 안전 printf
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();
}
// 사용
auto msg = format("User {} has {} points", "Alice", 100);
// "User Alice has 100 points"
패턴 3: 이벤트 발행
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: 가변 인자 템플릿은 언제 사용하나요?
A:
- 인자 개수가 가변적인 함수:
printf,make_tuple - 제네릭 라이브러리: 팩토리 함수, 래퍼
- 메타프로그래밍: 타입 리스트 처리
template<typename... Args>
void print(Args... args) {
(std::cout << ... << args) << '\n';
}
Q2: 재귀 vs fold expression?
A: C++17 이상이면 fold expression이 더 간결하고 빠릅니다.
// 재귀 (C++11)
template<typename T>
T sum(T value) { return value; }
template<typename T, typename... Args>
T sum(T first, Args... rest) {
return first + sum(rest...);
}
// Fold expression (C++17)
template<typename... Args>
auto sum(Args... args) {
return (... + args);
}
Q3: 가변 인자 템플릿 vs 가변 인자 함수?
A:
- 템플릿: 타입 안전, 컴파일 타임, 권장
- 함수(
...): 타입 불안전, 런타임, 비권장
// ❌ C 스타일: 타입 불안전
void print(int count, ...) {
va_list args;
// 타입을 알 수 없음
}
// ✅ 템플릿: 타입 안전
template<typename... Args>
void print(Args... args) {
(std::cout << ... << args);
}
Q4: 성능 오버헤드는?
A: 없습니다. 컴파일 타임에 모두 처리되므로 런타임 오버헤드가 없습니다.
template<typename... Args>
auto sum(Args... args) {
return (... + args);
}
sum(1, 2, 3, 4, 5);
// 컴파일 타임에 1 + 2 + 3 + 4 + 5로 확장
// 런타임 오버헤드 없음
Q5: 디버깅은 어떻게 하나요?
A:
static_assert: 컴파일 타임 검증- 컴파일 에러: 에러 메시지를 주의 깊게 읽기
- 단계적 테스트: 간단한 케이스부터 테스트
template<typename... Args>
void func(Args... args) {
static_assert(sizeof...(args) > 0, "최소 1개 인자 필요");
static_assert((std::is_integral_v<Args> && ...), "모두 정수 타입이어야 함");
}
Q6: sizeof… 연산자는 무엇인가요?
A: 파라미터 팩의 크기를 반환하는 연산자입니다. 컴파일 타임 상수입니다.
template<typename... Args>
void printCount(Args... args) {
std::cout << "인자 개수: " << sizeof...(args) << '\n';
}
printCount(1, 2, 3); // 3
Q7: 빈 팩은 어떻게 처리하나요?
A: 초기값을 제공하거나 if constexpr 로 확인합니다.
// 초기값 제공
template<typename... Args>
auto sum(Args... args) {
return (0 + ... + args); // 빈 팩이면 0
}
// if constexpr
template<typename... Args>
auto sum(Args... args) {
if constexpr (sizeof...(args) == 0) {
return 0;
} else {
return (... + args);
}
}
Q8: 가변 인자 템플릿 학습 리소스는?
A:
- “C++ Templates: The Complete Guide” by Vandevoorde, Josuttis, Gregor
- cppreference.com - Parameter pack
- Compiler Explorer - 템플릿 확장 확인
관련 글: variadic-template-advanced, fold-expression, perfect-forwarding.
한 줄 요약: 가변 인자 템플릿은 임의 개수의 타입 안전한 인자를 받을 수 있는 C++11 기능입니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ Fold Expressions | “파라미터 팩 접기” 가이드
- C++ 템플릿 | “제네릭 프로그래밍” 초보자 가이드
- C++ Fold Expressions | “Parameter Pack Folding” Guide
관련 글
- C++ Fold Expressions |
- C++ 템플릿 |
- C++ auto 타입 추론 | 복잡한 타입을 컴파일러에 맡기기
- C++ CTAD |
- C++20 Concepts 완벽 가이드 | 템플릿 제약의 새 시대