C++ 가변 인자 템플릿 | "가변 템플릿" 완벽 가이드

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:

관련 글: variadic-template-advanced, fold-expression, perfect-forwarding.

한 줄 요약: 가변 인자 템플릿은 임의 개수의 타입 안전한 인자를 받을 수 있는 C++11 기능입니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

관련 글

  • C++ Fold Expressions |
  • C++ 템플릿 |
  • C++ auto 타입 추론 | 복잡한 타입을 컴파일러에 맡기기
  • C++ CTAD |
  • C++20 Concepts 완벽 가이드 | 템플릿 제약의 새 시대