가변 인자 템플릿·Parameter Pack·Fold Expression 심화
개요 — 가변 인자 템플릿이란
Section titled “개요 — 가변 인자 템플릿이란”가변 인자 템플릿(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): 팩에 이항 연산자를 반복 적용
1. Parameter Pack 기본 문법
Section titled “1. Parameter Pack 기본 문법”// 타입 파라미터 팩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); // 인자 개수: 42. Pack Expansion — 팩 펼치기
Section titled “2. Pack Expansion — 팩 펼치기”팩 뒤에 ...을 붙이면 팩의 각 원소를 쉼표로 구분해 나열합니다.
// 팩 확장 컨텍스트 예시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)...);}재귀 펼치기 패턴 (C++11/14)
Section titled “재귀 펼치기 패턴 (C++11/14)”// 베이스 케이스 — 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, 13. Fold Expression (C++17) — 재귀 없이 팩 연산
Section titled “3. Fold Expression (C++17) — 재귀 없이 팩 연산”폴드 표현식은 팩에 이항 연산자를 반복 적용합니다. 재귀 없이 한 줄로 표현 가능합니다.
네 가지 폴드 형태
Section titled “네 가지 폴드 형태”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);}실용적인 폴드 표현식 예시
Section titled “실용적인 폴드 표현식 예시”// 출력 — 쉼표 연산자 폴드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); // falseAnyTrue(false, false, true); // trueSum(1, 2, 3, 4, 5); // 154. Perfect Forwarding과 결합
Section titled “4. Perfect Forwarding과 결합”// 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"); // nullopt5. 튜플 구현 원리
Section titled “5. 튜플 구현 원리”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"; // 42std::cout << Get<2>(t) << "\n"; // hello6. 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"; });7. 타입 안전 printf
Section titled “7. 타입 안전 printf”// 타입 안전 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);8. 연산 타입 추론
Section titled “8. 연산 타입 추론”// 여러 타입의 공통 타입 추론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); // intauto r2 = SumAuto(1, 2.0, 3.0f); // doubleauto r3 = SumAuto(std::string("a"), std::string("b")); // string| 기법 | C++ 버전 | 설명 |
|---|---|---|
Parameter Pack typename... T | C++11 | 0개 이상의 타입 파라미터 |
Pack Expansion T... | C++11 | 팩을 쉼표 구분 나열로 펼침 |
sizeof...(T) | C++11 | 팩 원소 수 (컴파일 타임) |
| 재귀 펼치기 | C++11 | Head+Tail 분리로 재귀 처리 |
Fold Expression (... op pack) | C++17 | 재귀 없이 팩에 연산자 적용 |
std::index_sequence | C++14 | 튜플 원소 인덱스 순회 |
핵심 원칙:
- C++17 이상에서는 재귀 대신 폴드 표현식을 우선 사용합니다.
std::forward<Args>(args)...를 이용한 완벽 전달로 불필요한 복사를 방지합니다.sizeof...(Args)로 팩 크기를 체크해 빈 팩 처리 예외 상황을 방지합니다.