C++11 가변 템플릿 인자 활용법

개요

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 대비 단순하지만, 가변 템플릿을 활용해 임의 개수의 인자를 타입 안전하게 처리하는 패턴을 보여준다.

태그: C++11 Variadic Templates Template Parameter Pack SFINAE Fold Expression

5월 22일 14:43에 게시됨