Skip to content

가변 인자 템플릿·Parameter Pack·Fold Expression 심화

가변 인자 템플릿(Variadic Templates)은 C++11에서 도입된 기능으로, 임의 개수의 타입 또는 값 파라미터를 받는 템플릿을 작성할 수 있게 합니다. std::make_tuple, std::make_unique, printf의 타입 안전 대안 등 표준 라이브러리 핵심 기능 대부분이 이 기법으로 구현됩니다.

핵심 개념:

  • Parameter Pack: typename... Args — 0개 이상의 타입 목록
  • Pack Expansion: Args... — 팩을 펼쳐 개별 타입/값 나열
  • sizeof…(Args): 팩의 원소 개수 (컴파일 타임 상수)
  • Fold Expression (C++17): 팩에 이항 연산자를 반복 적용

// 타입 파라미터 팩
template<typename... Types>
struct TypeList {};
TypeList<> empty; // 0개
TypeList<int> one; // 1개
TypeList<int, double, std::string> three; // 3개
// 함수 템플릿의 타입 팩
template<typename... Args>
void PrintAll(Args... args); // Args = 타입 팩, args = 값 팩
// sizeof...로 개수 확인
template<typename... Args>
void CountArgs(Args... args)
{
constexpr size_t count = sizeof...(Args); // 컴파일 타임 상수
std::cout << "인자 개수: " << count << "\n";
}
CountArgs(1, "hello", 3.14, true); // 인자 개수: 4

팩 뒤에 ...을 붙이면 팩의 각 원소를 쉼표로 구분해 나열합니다.

// 팩 확장 컨텍스트 예시
template<typename... Args>
void Expand(Args... args)
{
// 함수 인자 — (arg1, arg2, ..., argN)
AnotherFunc(args...);
// 초기화 목록
std::vector<std::common_type_t<Args...>> vec = {args...};
// sizeof 적용
size_t sizes[] = {sizeof(Args)...}; // sizeof(int), sizeof(double), ...
// 타입 변환 — const Args&...
AnotherFunc(std::forward<Args>(args)...);
}
// 베이스 케이스 — 0개 인자
void Print() { std::cout << "\n"; }
// 재귀 케이스
template<typename First, typename... Rest>
void Print(First first, Rest... rest)
{
std::cout << first;
if constexpr (sizeof...(rest) > 0) std::cout << ", ";
Print(rest...); // 재귀: 팩에서 첫 번째 원소 제거
}
Print(1, 3.14, "hello", true);
// 출력: 1, 3.14, hello, 1

3. Fold Expression (C++17) — 재귀 없이 팩 연산

Section titled “3. Fold Expression (C++17) — 재귀 없이 팩 연산”

폴드 표현식은 팩에 이항 연산자를 반복 적용합니다. 재귀 없이 한 줄로 표현 가능합니다.

template<typename... Args>
auto Sum(Args... args)
{
// 단항 좌폴드: (((args[0] op args[1]) op args[2]) ... op args[N])
return (... + args);
}
// 단항 우폴드: (args[0] op (args[1] op (... op args[N])))
template<typename... Args>
auto SumRight(Args... args)
{
return (args + ...);
}
// 이항 좌폴드: (((init op args[0]) op args[1]) ... op args[N])
template<typename... Args>
auto SumFromZero(Args... args)
{
return (0 + ... + args);
}
// 이항 우폴드: (args[0] op (args[1] op (... op (args[N] op init))))
template<typename... Args>
auto ProductFromOne(Args... args)
{
return (args * ... * 1);
}
// 출력 — 쉼표 연산자 폴드
template<typename... Args>
void PrintAll(Args&&... args)
{
((std::cout << args << " "), ...);
std::cout << "\n";
}
// 논리 연산
template<typename... Args>
bool AllTrue(Args... args) { return (... && args); }
template<typename... Args>
bool AnyTrue(Args... args) { return (... || args); }
// 합계
template<typename... Args>
auto Sum(Args... args) { return (... + args); }
// 최대값
template<typename T, typename... Args>
T Max(T first, Args... rest)
{
return (first > ... > rest) ? first : std::max({static_cast<T>(rest)...});
// 실제로는 이렇게:
T result = first;
((result = result > rest ? result : rest), ...);
return result;
}
AllTrue(true, true, false); // false
AnyTrue(false, false, true); // true
Sum(1, 2, 3, 4, 5); // 15

// std::make_unique 스타일 — 완벽 전달
template<typename T, typename... Args>
std::unique_ptr<T> Make(Args&&... args)
{
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
// std::invoke 스타일 — 함수 + 인자 완벽 전달
template<typename F, typename... Args>
decltype(auto) Invoke(F&& func, Args&&... args)
{
return std::forward<F>(func)(std::forward<Args>(args)...);
}
// 에러 처리 래퍼
template<typename F, typename... Args>
auto TryCatch(F&& func, Args&&... args) -> std::optional<decltype(func(args...))>
{
try {
return std::forward<F>(func)(std::forward<Args>(args)...);
} catch (...) {
return std::nullopt;
}
}
auto result = TryCatch(std::stoi, "123"); // optional<int>{123}
auto fail = TryCatch(std::stoi, "abc"); // nullopt

std::tuple은 재귀적 가변 인자 템플릿으로 구현됩니다.

// 간단한 튜플 구현 (재귀 상속)
template<typename... Types>
struct Tuple {};
// 재귀 케이스: 첫 원소 + 나머지
template<typename Head, typename... Tail>
struct Tuple<Head, Tail...> : Tuple<Tail...>
{
Head head;
Tuple(Head h, Tail... t) : Tuple<Tail...>(t...), head(std::move(h)) {}
};
// 인덱스 기반 접근
template<size_t I, typename Head, typename... Tail>
auto& Get(Tuple<Head, Tail...>& t)
{
if constexpr (I == 0) return t.head;
else return Get<I-1>(static_cast<Tuple<Tail...>&>(t));
}
Tuple<int, double, std::string> t(42, 3.14, "hello");
std::cout << Get<0>(t) << "\n"; // 42
std::cout << Get<2>(t) << "\n"; // hello

6. Index Sequence — 팩 인덱스 접근

Section titled “6. Index Sequence — 팩 인덱스 접근”
// std::index_sequence로 튜플 원소 순회
template<typename Tuple, size_t... Is>
void PrintTupleImpl(const Tuple& t, std::index_sequence<Is...>)
{
// 폴드 표현식으로 각 인덱스 출력
((std::cout << std::get<Is>(t) << " "), ...);
}
template<typename... Args>
void PrintTuple(const std::tuple<Args...>& t)
{
PrintTupleImpl(t, std::make_index_sequence<sizeof...(Args)>{});
}
auto t = std::make_tuple(1, 3.14, "hello");
PrintTuple(t); // 1 3.14 hello
// 함수를 모든 튜플 원소에 적용
template<typename F, typename Tuple, size_t... Is>
void ForEachImpl(Tuple& t, F&& f, std::index_sequence<Is...>)
{
(f(std::get<Is>(t)), ...);
}
template<typename F, typename... Args>
void ForEach(std::tuple<Args...>& t, F&& f)
{
ForEachImpl(t, std::forward<F>(f),
std::make_index_sequence<sizeof...(Args)>{});
}
auto tup = std::make_tuple(1, 2.5, std::string("hi"));
ForEach(tup, [](auto& v) { std::cout << v << "\n"; });

// 타입 안전 printf — 재귀 가변 인자 템플릿
void SafePrintf(const char* fmt)
{
while (*fmt)
{
if (*fmt == '%' && *(fmt+1) != '%')
throw std::runtime_error("인자 부족");
std::cout << *fmt++;
}
}
template<typename T, typename... Rest>
void SafePrintf(const char* fmt, T value, Rest... rest)
{
while (*fmt)
{
if (*fmt == '%' && *(fmt+1) != '%')
{
std::cout << value;
SafePrintf(fmt + 2, rest...);
return;
}
std::cout << *fmt++;
}
}
SafePrintf("이름: %, 나이: %, 점수: %\n", "Alice", 30, 95.5);

// 여러 타입의 공통 타입 추론
template<typename... Args>
using CommonType = std::common_type_t<Args...>;
CommonType<int, double, float> r; // double
// 반환 타입 추론
template<typename... Args>
auto SumAuto(Args... args)
{
return (args + ...); // 반환 타입은 연산 결과 타입
}
auto r1 = SumAuto(1, 2, 3); // int
auto r2 = SumAuto(1, 2.0, 3.0f); // double
auto r3 = SumAuto(std::string("a"), std::string("b")); // string

기법C++ 버전설명
Parameter Pack typename... TC++110개 이상의 타입 파라미터
Pack Expansion T...C++11팩을 쉼표 구분 나열로 펼침
sizeof...(T)C++11팩 원소 수 (컴파일 타임)
재귀 펼치기C++11Head+Tail 분리로 재귀 처리
Fold Expression (... op pack)C++17재귀 없이 팩에 연산자 적용
std::index_sequenceC++14튜플 원소 인덱스 순회

핵심 원칙:

  • C++17 이상에서는 재귀 대신 폴드 표현식을 우선 사용합니다.
  • std::forward<Args>(args)...를 이용한 완벽 전달로 불필요한 복사를 방지합니다.
  • sizeof...(Args)로 팩 크기를 체크해 빈 팩 처리 예외 상황을 방지합니다.