팩
팩 (Pack) (C++11부터)
**팩(pack)**은 다음 중 하나를 정의하는 C++ 개체예요.
- 매개변수 팩(parameter pack)
- 템플릿 매개변수 팩(template parameter pack)
- 함수 매개변수 팩(function parameter pack)
- 람다 init-capture 팩(lambda init-capture pack) (C++20부터)
- 구조적 바인딩 팩(structured binding pack) (C++26부터)
템플릿 매개변수 팩은 0개 이상의 템플릿 인자(비타입·타입·템플릿)를 받아들이는 템플릿 매개변수예요. 함수 매개변수 팩은 0개 이상의 함수 인자를 받아들이는 함수 매개변수예요. 람다 init-capture 팩은 초기화자의 팩 확장에 있는 각 요소에 대해 init-capture를 도입하는 람다 캡처이고, 구조적 바인딩 팩은 구조적 바인딩 선언에서 0개 이상의 구조적 바인딩을 도입하는 식별자예요.
팩의 요소 수는 다음과 같아요.
- 팩이 템플릿·함수 매개변수 팩이면, 해당 매개변수 팩에 제공된 인자의 수,
- 팩이 람다 init-capture 팩이면, 초기화자의 팩 확장에 있는 요소의 수, (C++20부터)
- 팩이 구조적 바인딩 팩이면, 구조적 바인딩 선언에 있는 비팩 요소 수를 뺀 초기화자의 구조적 바인딩 크기. (C++26부터)
매개변수 팩이 하나 이상 있는 템플릿을 **가변 템플릿(variadic template)**이라고 불러요.
출처: cppreference
본문
문법(Syntax)
템플릿 매개변수 팩(별칭 템플릿, 클래스 템플릿, 변수 템플릿(C++14부터), 개념(C++20부터), 함수 템플릿 매개변수 목록에 나타남):
type ... pack-name (선택) |
(1) | |
type-parameter-key ... pack-name (선택) |
(2) | |
type-constraint ... pack-name (선택) |
(3) | (C++20부터) |
template < parameter-list > class ... pack-name (선택) |
(4) | (C++17까지) |
template < parameter-list > type-parameter-key ... pack-name (선택) |
(4) | (C++17부터) |
함수 매개변수 팩(declarator의 한 형태, 가변 함수 템플릿의 함수 매개변수 목록에 나타남):
pack-name ... pack-param-name (선택) |
(5) |
비매개변수 팩의 문법은 람다 init-capture 팩과 구조적 바인딩 팩(C++26부터)을 참고해요.
팩 확장(템플릿 본문에 나타남):
pattern ... |
(6) |
- 이름이 있어도 되고 없어도 되는 비타입 템플릿 매개변수 팩
- 이름이 있어도 되고 없어도 되는 타입 템플릿 매개변수 팩
- 제약된(constrained) 타입 템플릿 매개변수 팩, 이름 선택 (C++20부터)
- 이름이 있어도 되고 없어도 되는 템플릿 템플릿 매개변수 팩
- 이름이 있어도 되고 없어도 되는 함수 매개변수 팩
- 팩 확장: 0개 이상의
pattern목록으로 확장됨. 패턴은 팩을 적어도 하나 포함해야 해요.
설명(Explanation)
가변 클래스 템플릿은 어떤 수의 템플릿 인자로도 인스턴스화될 수 있어요.
template<class... Types>
struct Tuple {};
Tuple<> t0; // Types에 인자 없음
Tuple<int> t1; // Types에 인자 하나: int
Tuple<int, float> t2; // Types에 인자 둘: int와 float
Tuple<0> t3; // 에러: 0은 타입이 아님
가변 함수 템플릿은 어떤 수의 함수 인자로도 호출될 수 있어요(템플릿 인자는 템플릿 인자 추론으로 추론됨):
template<class... Types>
void f(Types... args);
f(); // OK: args에 인자 없음
f(1); // OK: args에 인자 하나: int
f(2, 1.0); // OK: args에 인자 둘: int와 double
기본 클래스 템플릿에서는 템플릿 매개변수 팩이 템플릿 매개변수 목록의 마지막 매개변수여야 해요. 함수 템플릿에서는, 뒤따르는 모든 매개변수가 함수 인자로 추론되거나 기본 인자를 가지면 템플릿 매개변수 팩이 목록의 앞쪽에 나올 수 있어요:
template<typename U, typename... Ts> // OK: U를 추론할 수 있음
struct valid;
// template<typename... Ts, typename U> // 에러: Ts...가 끝이 아님
// struct Invalid;
template<typename... Ts, typename U, typename=void>
void valid(U, Ts...); // OK: U를 추론할 수 있음
// void valid(Ts..., U); // 사용 불가: 이 위치에서 Ts...는 비추론 문맥임
valid(1.0, 1, 2, 3); // OK: U를 double, Ts를 {int, int, int}로 추론함
가변 템플릿의 모든 유효한 특수화가 빈 템플릿 매개변수 팩을 요구하면, 프로그램은 ill-formed입니다(진단 필요 없음, IFNDR).
팩 확장(Pack expansion)
적어도 하나의 팩 이름이 한 번 이상 나타나는 패턴 뒤에 줄임표(ellipsis)를 붙이면, 그 패턴이 0개 이상의 인스턴스화로 확장돼요. 이때 팩의 이름이 팩의 각 요소로 (순서대로) 치환돼요. 정렬 지정자의 인스턴스화는 공백으로 구분되고, 그 외 인스턴스화는 쉼표로 구분돼요.
template<class... Us>
void f(Us... pargs) {}
template<class... Ts>
void g(Ts... args)
{
f(&args...); // “&args...”는 팩 확장
// “&args”가 그 패턴
}
g(1, 0.2, "a"); // Ts... args는 int E1, double E2, const char* E3로 확장됨
// &args...는 &E1, &E2, &E3로 확장됨
// Us... pargs는 int* E1, double* E2, const char** E3로 확장됨
같은 패턴에 두 팩의 이름이 나타나면 동시에 확장되며, 두 팩의 길이는 같아야 해요:
template<typename...>
struct Tuple {};
template<typename T1, typename T2>
struct Pair {};
template<class... Args1>
struct zip
{
template<class... Args2>
struct with
{
typedef Tuple<Pair<Args1, Args2>...> type;
// Pair<Args1, Args2>...가 팩 확장
// Pair<Args1, Args2>가 패턴
};
};
typedef zip<short, int>::with<unsigned short, unsigned>::type T1;
// Pair<Args1, Args2>...는
// Pair<short, unsigned short>, Pair<int, unsigned int>로 확장됨
// T1은 Tuple<Pair<short, unsigned short>, Pair<int, unsigned>>
// typedef zip<short>::with<unsigned short, unsigned>::type T2;
// 에러: 팩 확장이 서로 다른 길이의 팩을 포함함
팩 확장이 다른 팩 확장 안에 중첩되면, 가장 안쪽 팩 확장 안에 나타난 팩들은 그것이 확장하고, 바깥 팩 확장에는 (가장 안쪽이 아닌) 다른 팩이 언급돼 있어야 해요:
template<class... Args>
void g(Args... args)
{
f(const_cast<const Args*>(&args)...);
// const_cast<const Args*>(&args)가 패턴, 두 팩(Args와 args)을 동시에 확장함
f(h(args...) + args...); // 중첩 팩 확장:
// 안쪽 팩 확장은 "args...", 먼저 확장됨
// 바깥 팩 확장은 h(E1, E2, E3) + args..., 나중에 확장됨
// (h(E1, E2, E3) + E1, h(E1, E2, E3) + E2, h(E1, E2, E3) + E3)
}
팩의 요소 수가 0일 때(빈 팩), 팩 확장의 인스턴스화는 둘러싼 구조의 구문 해석을 바꾸지 않아요. 팩 확장을 통째로 생략하면 ill-formed가 되거나 구문 모호성이 생길 상황에서도 마찬가지고요. 인스턴스화는 빈 목록을 만들어요.
template<class... Bases>
struct X : Bases... { };
template<class... Args>
void f(Args... args)
{
X<Args...> x(args...);
}
template void f<>(); // OK, X<>에는 기반 클래스가 없음
// x는 값-초기화된 X<> 타입의 변수
확장 위치(Expansion loci)
확장이 일어나는 위치에 따라, 결과로 나오는 쉼표 구분(정렬 지정자는 공백 구분) 목록은 함수 매개변수 목록, 멤버 초기화 목록, 속성 목록 등 서로 다른 종류의 목록이 돼요. 허용되는 모든 문맥은 다음과 같아요.
함수 인자 목록(Function argument lists)
팩 확장은 함수 호출 연산자의 괄호 안에 나타날 수 있어요. 이때 줄임표 왼쪽의 가장 큰 표현식 또는 중괄호로 감싼 초기화자 목록이 확장되는 패턴이 돼요:
f(args...); // f(E1, E2, E3)로 확장됨
f(&args...); // f(&E1, &E2, &E3)로 확장됨
f(n, ++args...); // f(n, ++E1, ++E2, ++E3)로 확장됨
f(++args..., n); // f(++E1, ++E2, ++E3, n)로 확장됨
f(const_cast<const Args*>(&args)...);
// f(const_cast<const E1*>(&X1), const_cast<const E2*>(&X2), const_cast<const E3*>(&X3))
f(h(args...) + args...); // 로 확장됨
// f(h(E1, E2, E3) + E1, h(E1, E2, E3) + E2, h(E1, E2, E3) + E3)
괄호로 감싼 초기화자(Parenthesized initializers)
팩 확장은 직접 초기화자(direct initializer), 함수형 캐스트, 기타 문맥(멤버 초기화자, new 표현식 등)의 괄호 안에 나타날 수 있어요. 이 경우 규칙은 위의 함수 호출 표현식과 동일해요:
Class c1(&args...); // Class::Class(&E1, &E2, &E3) 호출
Class c2 = Class(n, ++args...); // Class::Class(n, ++E1, ++E2, ++E3) 호출
::new((void *)p) U(std::forward<Args>(args)...) // std::allocator::allocate
중괄호로 감싼 초기화자(Brace-enclosed initializers)
중괄호로 감싼 초기화자 목록에도 팩 확장이 나타날 수 있어요:
template<typename... Ts>
void func(Ts... args)
{
const int size = sizeof...(args) + 2;
int res[size] = {1, args..., 2};
// 초기화자 목록은 순서를 보장하므로, 이것으로 팩의 각 요소에
// 순서대로 함수를 호출할 수 있음:
int dummy[sizeof...(Ts)] = {(std::cout << args, 0)...};
}
템플릿 인자 목록(Template argument lists)
팩 확장은 템플릿이 확장에 맞는 매개변수를 가지기만 하면 템플릿 인자 목록 어디서든 쓸 수 있어요:
template<class A, class B, class... C>
void func(A arg1, B arg2, C... arg3)
{
container<A, B, C...> t1; // container<A, B, E1, E2, E3>로 확장됨
container<C..., A, B> t2; // container<E1, E2, E3, A, B>로 확장됨
container<A, C..., B> t3; // container<A, E1, E2, E3, B>로 확장됨
}
함수 매개변수 목록(Function parameter list)
함수 매개변수 목록에서 매개변수 선언에 줄임표가 나타나면(함수 매개변수 팩을 이름 붙이든 말든, Args ... args처럼), 그 매개변수 선언이 패턴이 돼요:
template<typename... Ts>
void f(Ts...) {}
f('a', 1); // Ts...는 void f(char, int)로 확장됨
f(0.1); // Ts...는 void f(double)로 확장됨
template<typename... Ts, int... N>
void g(Ts (&...arr)[N]) {}
int n[1];
g<const char, int>("a", n); // Ts (&...arr)[N]는
// const char (&)[2], int(&)[1]로 확장됨
참고: 패턴 Ts (&...arr)[N]에서 줄임표는 다른 모든 팩 확장처럼 마지막 요소가 아니라 가장 안쪽 요소예요.
참고: Ts (&...)[N]는 허용되지 않아요. C++11 문법이 괄호로 감싼 줄임표에 이름을 요구하기 때문이에요(CWG issue 1488).
템플릿 매개변수 목록(Template parameter list)
팩 확장은 템플릿 매개변수 목록에 나타날 수 있어요:
template<typename... T>
struct value_holder
{
template<T... Values> // <int, char, int(&)[5]> 같은
struct apply {}; // 비타입 템플릿 매개변수 목록으로 확장됨
};
기반 지정자와 멤버 초기화자 목록(Base specifiers and member initializer lists)
팩 확장은 클래스 선언의 기반 클래스 목록을 지정할 수 있어요. 보통 이것은 생성자가 이 기반들의 생성자를 호출하도록 멤버 초기화자 목록에서 팩 확장을 써야 한다는 뜻이기도 해요:
template<class... Mixins>
class X : public Mixins...
{
public:
X(const Mixins&... mixins) : Mixins(mixins)... {}
};
람다 캡처(Lambda captures)
팩 확장은 람다(lambda) 표현식의 캡처 절에 나타날 수 있어요:
template<class... Args>
void f(Args... args)
{
auto lm = [&, args...] { return g(args...); };
lm();
}
sizeof... 연산자
sizeof... 연산자도 팩 확장으로 분류돼요:
template<class... Types>
struct count
{
static const std::size_t value = sizeof...(Types);
};
동적 예외 명세(Dynamic exception specifications) (C++17까지)
동적 예외 명세의 예외 목록도 팩 확장일 수 있어요:
template<class... X>
void func(int arg) throw(X...)
{
// ... 상황에 따라 서로 다른 X를 throw
}
정렬 지정자(Alignment specifier)
팩 확장은 alignas 키워드가 쓰는 타입 목록과 표현식 목록 모두에 허용돼요. 인스턴스화는 공백으로 구분돼요:
template<class... T>
struct Align
{
alignas(T...) unsigned char buffer[128];
};
Align<int, short> a; // 확장 후 정렬 지정자는
// alignas(int) alignas(short)
// (사이에 쉼표 없음)
속성 목록(Attribute list)
팩 확장은 속성의 명세가 허용하면 속성(attributes) 목록에 허용돼요. 예:
template<int... args>
[[vendor::attr(args)...]] void* f();
폴드 표현식(Fold expressions) (C++17부터)
폴드 표현식에서 패턴은 펼쳐지지 않은 팩을 포함하지 않는 전체 부분표현식이에요.
using 선언(Using-declarations) (C++17부터)
using 선언에서 줄임표가 declarator 목록에 나타날 수 있어요. 템플릿 매개변수 팩에서 파생할 때 유용해요:
template<typename... bases>
struct X : bases...
{
using bases::g...;
};
X<B, D> x; // OK: B::g와 D::g가 도입됨
팩 인덱싱(Pack indexing) (C++26부터)
팩 인덱싱에서 팩 확장은 펼쳐지지 않은 팩 뒤에 줄임표와 첨자가 오는 형태예요. 팩 인덱싱 표현식의 패턴은 식별자이고, 팩 인덱싱 지정자의 패턴은 typedef-name이에요.
consteval auto first_plus_last(auto... args)
{
return args...[0] + args...[sizeof...(args) - 1];
}
static_assert(first_plus_last(5) == 10);
static_assert(first_plus_last(5, 4) == 9);
static_assert(first_plus_last(5, 6, 2) == 7);
friend 선언(Friend declarations) (C++26부터)
클래스 friend 선언에서 각 타입 지정자 뒤에 줄임표가 올 수 있어요:
struct C {};
struct E { struct Nested; };
template<class... Ts>
class R
{
friend Ts...;
};
template<class... Ts, class... Us>
class R<R<Ts...>, R<Us...>>
{
friend Ts::Nested..., Us...;
};
R<C, E> rce; // 클래스 C와 E는 R<C, E>의 friend
R<R<E>, R<C, int>> rr; // E::Nested와 C는 R<R<E>, R<C, int>>의 friend
폴드 확장 제약(Fold expanded constraints) (C++26부터)
폴드 확장 제약에서 패턴은 그 폴드 확장 제약의 제약이에요. 폴드 확장 제약은 인스턴스화되지 않아요.
어노테이션 목록(Annotation list) (C++26부터)
팩 확장은 어노테이션(annotations) 목록에 허용돼요:
template<auto... Ann>
[[=Ann...]] int x;
노트(Notes)
이 절은 아직 불완전해요 (개별 요소에 접근하는 부분 특수화와 그 외 방법 관련).
| 기능 테스트 매크로 | 값 | 표준 | 기능 |
|---|---|---|---|
__cpp_variadic_templates |
200704L |
(C++11) | 가변 템플릿 |
__cpp_pack_indexing |
202311L |
(C++26) | 팩 인덱싱 |
예제(Example)
아래 예제는 std::printf와 비슷한 함수를 정의해요. 이 함수는 형식 문자열에서 문자 %가 나타나는 자리를 값으로 바꿔 치환해요.
첫 번째 오버로드는 형식 문자열만 전달되고 매개변수 확장이 없을 때 호출돼요.
두 번째 오버로드는 인자의 머리(head)를 위한 별도의 템플릿 매개변수와 매개변수 팩을 포함해요. 덕분에 재귀 호출이 매개변수의 꼬리(tail)만 비워질 때까지 전달할 수 있어요.
Targs는 템플릿 매개변수 팩이고 Fargs는 함수 매개변수 팩이에요.
#include <iostream>
void tprintf(const char* format) // 기본 함수
{
std::cout << format;
}
template<typename T, typename... Targs>
void tprintf(const char* format, T value, Targs... Fargs) // 재귀 가변 함수
{
for (; *format != '\0'; format++)
{
if (*format == '%')
{
std::cout << value;
tprintf(format + 1, Fargs...); // 재귀 호출
return;
}
std::cout << *format;
}
}
int main()
{
tprintf("% world% %\n", "Hello", '!', 123);
}
출력:
Hello world! 123
결함 보고(Defect reports)
이전에 발표된 C++ 표준들에 소급 적용된 동작 변경 결함 보고들이 있어요.
| DR | 적용 대상 | 발표 당시 동작 | 올바른 동작 |
|---|---|---|---|
| CWG 1533 | C++11 | 팩 확장이 (멤버가 아니라) 멤버 초기화자에서 일어날 수 있었음 | 허용되지 않음 |
| CWG 2717 | C++11 | 정렬 지정자의 인스턴스화가 쉼표로 구분됨 | 공백으로 구분됨 |