개요
C++11에서 도입된 가변 템플릿(Variadic Templates)은 고정된 개수의 인자에 얽매이지 않고 유연한 코드를 작성할 수 있게 해주는 핵심 기능이다. 이 글에서는 파라미터 팩의 다양한 전개 방식과 실무 활용 예시를 살펴다.
파라미터 팩 기본 구조
가변 템플릿은 타입 팩과 값 팩 두 가지 형태로 사용할 수 있다. typename... 또는 class... 구문으로 타입 파라미터 팩을 선언하며, 구체적인 타입이나 값으로 전개된다.
// 타입 파라미터 팩을 활용한 컨테이너
template<typename... Elements>
class Container {
// Elements는 0개 이상의 타입으로 전개됨
};
// 사용
Container<int, float, const char*> data;전개 전략 1: 재귀 템플릿 함수
가장 직관적인 방법은 재귀를 통해 파라미터 팩을 한 개씩 소비하는 것이다. 종료 조건을 위한 베이스 케이스를 별도로 정의해야 한다.
#include <iostream>
// 종료 케이스: 단일 인자
template <typename U>
void display(const U& value) {
std::cout << "[" << value << "]" << std::endl;
}
// 가변 인자 버전: 첫 번째 인자를 분리 후 나머지 재귀
template <typename U, typename... Remaining>
void display(const U& value, Remaining... rest) {
std::cout << "[" << value << "]";
display(rest...); // 파라미터 팩 축소
}
int main() {
display(42, 3.14, "C++", 'K');
// 출력: [42][3.14][C++][K]
return 0;
}전개 과정을 추적하면 다음과 같다: display(42, 3.14, "C++", 'K') → display(3.14, "C++", 'K') → display("C++", 'K') → display('K') → 종료.
전개 전략 2: 컴파일 타임 폴드
C++17 이전에는 괴기법(괄호 + 쉼표)을 활용한 트릭이 널리 사용되었다. 이 방식은 초기화자 리스트의 평가 순서를 보장하는 특성을 이용한다.
#include <iostream>
template <typename... Ts>
void show_all(Ts... items) {
int dummy[] = { 0, ((std::cout << "<" << items << ">"), 0)... };
(void)dummy; // 미사용 경고 방지
std::cout << std::endl;
}
int main() {
show_all(100, 2.718, "fold");
// 출력: <100><2.718><fold>
}여기서 int dummy[]는 중괄호 초기화의 평가 순서가 왼쪽에서 오른쪽으로 보장된다는 점을 활용한다. 각 항목의 결과는 버리고, 부수 효과인 출력문만 실행된다.
전개 전략 3: SFINAE와 인덱스 시퀀스 활용
튜플이나 배열 같은 집합 자료구조를 순회할 때는 인덱스를 생성하는 방식이 유용하다. std::index_sequence 또는 직접 구현한 인덱스 팩을 사용한다.
#include <iostream>
#include <tuple>
#include <type_traits>
// 인덱스 시퀀스 생성 헬퍼 (C++14 이전 직접 구현)
template<size_t... Is>
struct idx_seq {};
// 종료: 인덱스가 범위를 벗어나면 SFINAE로 제외
template<size_t Pos, typename Tuple>
typename std::enable_if<(Pos >= std::tuple_size<Tuple>::value)>::type
traverse_impl(const Tuple&) {
std::cout << std::endl;
}
// 진행: 현재 위치 출력 후 다음 인덱스로
template<size_t Pos, typename Tuple>
typename std::enable_if<(Pos < std::tuple_size<Tuple>::value)>::type
traverse_impl(const Tuple& t) {
std::cout << "{" << std::get<Pos>(t) << "}";
traverse_impl<Pos + 1>(t);
}
template<typename... Ts>
void dump_pack(Ts... vals) {
auto packed = std::make_tuple(vals...);
traverse_impl<0>(packed);
}
int main() {
dump_pack(7, 1.414, "tuple");
// 출력: {7}{1.414}{tuple}
return 0;
}이 패턴의 핵심은 std::enable_if를 통해 조건부 오버로딩을 구현하는 것이다. 컴파일러는 Pos 값에 따라 두 오버로드 중 하나를 선택하며, 불가능한 쪽은 SFINAE 원칙에 의해 후보에서 제외된다.
전개 전략 4: C++17 폴드 표현식
C++17부터는 괴기법 없이 깔끔한 구문으로 파라미터 팩을 전개할 수 있다. 단항 우항 폴드, 이항 폴드 등 다양한 형태가 있다.
#include <iostream>
template <typename... Args>
void modern_print(Args... args) {
((std::cout << "[" << args << "]"), ...); // 우항 단항 폴드
std::cout << std::endl;
}
int main() {
modern_print(1, 2, 3, "end");
// 출력: [1][2][3][end]
}폴드 표현식의 (expr, ...) 문법은 각 args에 대해 expr을 순차적으로 실행하며, 쉼표 연산자의 특성상 최종 결과는 마지막 표현식의 값이 된다.
실무 활용: 타입 안전한 포맷팅
가변 템플릿의 대표적인 활용 사례는 타입 안전성을 보장하는 출력 유틸리티를 만드는 것이다. printf의 포맷 문자열 버그를 원천 차단할 수 있다.
#include <iostream>
#include <sstream>
#include <string>
template <typename T>
std::string stringify(const T& v) {
std::ostringstream oss;
oss << v;
return oss.str();
}
template <typename... Items>
std::string format_message(const std::string& pattern, Items... items) {
std::string result = pattern;
std::string replacements[] = { stringify(items)... };
for (const auto& rep : replacements) {
size_t pos = result.find("{}");
if (pos != std::string::npos) {
result.replace(pos, 2, rep);
}
}
return result;
}이 구현은 완전한 format 대비 단순하지만, 가변 템플릿을 활용해 임의 개수의 인자를 타입 안전하게 처리하는 패턴을 보여준다.