팩 인덱싱
팩 인덱싱 (Pack indexing) (C++26부터)
템플릿에서 팩을 다루다 보면 "이 팩의 N번째 요소에 접근하고 싶다"는 요구가 생기는데, C++26 전에는 별도의 보조 템플릿(재귀나 std::tuple_element 같은)을 거쳐야 했어요. **팩 인덱싱(pack indexing)**은 팩에 첨자를 붙여 P...[I]처럼 바로 그 요소에 접근하는 문법이에요.
팩 인덱싱은 펼쳐지지 않은 팩 뒤에 줄임표와 첨자(subscript)가 오는 _팩 확장_이에요. 두 종류가 있어요: **팩 인덱싱 표현식(pack indexing expression)**과 팩 인덱싱 지정자(pack indexing specifier).
P가 P₀, P₁, ..., Pₙ₋₁을 포함하는 비어 있지 않은 팩이고 I가 유효한 인덱스라면, 확장 P...[I]의 인스턴스화는 P의 팩 요소 Pᵢ를 산출해요.
비상수 표현식 인덱스 I로 팩을 인덱싱하는 것은 허용되지 않아요.
int runtime_idx();
void bar(auto... args)
{
auto a = args...[0];
const int n = 1;
auto b = args...[n];
int m = 2;
auto c = args...[m]; // 에러: 'm'은 상수 표현식이 아님
auto d = args...[runtime_idx()]; // 에러: 'runtime_idx()'는 상수 표현식이 아님
}
템플릿 템플릿 매개변수로 이뤄진 팩은 인덱싱할 수 없어요.
template<template<typename...> typename... Temps>
using A = Temps...[0]<>; // 에러: 'Temps'는 템플릿 템플릿 매개변수 팩임
template<template<typename...> typename... Temps>
using B = Temps<>...[0]; // 에러: 'Temps<>'는 팩 이름을 나타내지 않음
// 단순 템플릿 id(simple-template-id)임에도 불구하고
출처: cppreference
본문
문법(Syntax)
id-expression ...[ expression ] |
(1) | |
typedef-name ...[ expression ] |
(2) |
- 팩 인덱싱 표현식
- 팩 인덱싱 지정자
typedef-name |
팩을 이름 짓는 식별자 또는 단순 템플릿 id |
expression |
인덱스(index)로 지칭되는 std::size_t 타입의 변환된 상수 표현식 I. 팩 인덱싱의 어떤 팩 P에 대해 I가 범위 [0, sizeof...(P)) 안에 있어야 함 |
팩 인덱싱 표현식(Pack indexing expression)
id-expression ...[ expression ] |
팩 인덱싱 표현식은 팩 요소 Pᵢ의 표현식인 id-expression 을 나타내요. id-expression은 다음 중 하나의 선언에 의해 도입돼야 해요:
- 비타입 템플릿 매개변수 팩,
- 함수 매개변수 팩,
- 람다 init-capture 팩,
- 구조적 바인딩 팩.
template<std::size_t I, typename... Ts>
constexpr auto element_at(Ts... args)
{
// 'args'는 함수 매개변수 팩 선언에 의해 도입됨
return args...[I];
}
static_assert(element_at<0>(3, 5, 9) == 3);
static_assert(element_at<2>(3, 5, 9) == 9);
id-expression이 아닌 복잡한 표현식의 팩을 인덱싱하는 것은 허용되지 않아요.
template<std::size_t I, auto... Vals>
constexpr auto identity_at = (Vals)...[I]; // 에러
// 'Vals...[I]'를 사용할 것
template<std::size_t I, std::size_t... Vals>
constexpr std::size_t triple_at = (Vals * 3)...[I]; // 에러
// 'Vals...[I] * 3'을 사용할 것
template<std::size_t I, typename... Args>
constexpr decltype(auto) get(Args&&... args) noexcept
{
return std::forward<Args>(args)...[I]; // 에러
// 'std::forward<Args...[I]>(args...[I])'를 사용할 것
}
팩 인덱싱 표현식에 decltype을 적용하는 것은 id-expression에 decltype을 적용하는 것과 같아요.
void f()
{
{
using T0 = decltype(args...[0]); // 'T0'는 'double'
using T1 = decltype((args...[0])); // 'T1'은 'double&'
}(3.14);
}
팩 인덱싱 지정자(Pack indexing specifier)
typedef-name ...[ expression ] |
팩 인덱싱 지정자는 팩 요소 Pᵢ의 타입인 computed-type-specifier 를 나타내요. typedef-name은 타입 템플릿 매개변수 팩의 선언에 의해 도입돼야 해요.
template<typename... Ts>
using last_type_t = Ts...[sizeof...(Ts) - 1];
static_assert(std::is_same_v<last_type_t<>, int>); // 에러: 범위 밖
static_assert(std::is_same_v<last_type_t<int>, int>);
static_assert(std::is_same_v<last_type_t<bool, char>, char>);
static_assert(std::is_same_v<last_type_t<float, int, bool*>, bool*>);
팩 인덱싱 지정자는 다음으로 나타날 수 있어요:
- 단순 타입 지정자,
- 기반 클래스 지정자,
- 중첩 이름 지정자(nested-name-specifier),
- 명시적 소멸자 호출의 타입.
팩 인덱싱 지정자는 함수·생성자 매개변수 목록에서 템플릿 인자 추론의 비추론 문맥(non-deduced contexts)을 만들기 위해 사용될 수 있어요.
template<typename...>
struct type_seq {};
template<typename... Ts>
auto f(Ts...[0] arg, type_seq<Ts...>)
{
return arg;
}
// OK: "Hello"가 'std::string_view'로 암시 변환됨
std::same_as<std::string_view> auto a = f("Hello", type_seq<std::string_view>{});
// 에러: "Ok"는 'int'로 변환될 수 없음
std::same_as<int> auto b = f("Ok", type_seq<int, const char*>{});
노트(Notes)
C++26 이전에는 Ts...[N]이 크기 N의 이름 없는 배열들의 함수 매개변수 팩을 선언하는 유효한 문법이었고, 매개변수 타입은 포인터로 추가 조정됐어요. C++26부터는 Ts...[1]이 팩 인덱싱 지정자로 해석되므로 아래 동작이 #2로 바뀌어요.
첫 번째 동작을 유지하려면 함수 매개변수 팩에 이름을 붙이거나, 포인터 타입들의 팩으로 수동 조정해야 해요.
template<typename... Ts>
void f(Ts...[1]);
template<typename... Ts>
void g(Ts... args[1]);
template<typename... Ts>
void h(Ts*...); // 더 명확하지만 더 허용적: Ts...에는 cv void나 함수 타입이 올 수 있음
void foo()
{
f<char, bool>(nullptr, nullptr);
// 동작 #1 (C++26 이전):
// 'f<char, bool>(char*, bool*)'(aka 'f<char, bool>(char[1], bool[1])')를 호출
// 동작 #2 (C++26부터):
// 에러: 'f<char, bool>(bool)'이 호출되었다고 추정되지만
// 2개 인자가 1개 대신 제공됨
g<char, bool>(nullptr, nullptr);
// 'g<char, bool>(char*, bool*)'(aka 'g<char, bool>(char[1], bool[1])')를 호출
h<char, bool>(nullptr, nullptr);
// 'h<char, bool>(char*, bool*)'를 호출
}
| 기능 테스트 매크로 | 값 | 표준 | 기능 |
|---|---|---|---|
__cpp_pack_indexing |
202311L |
(C++26) | 팩 인덱싱 |
예제(Example)
[I...] 같은 중복 인덱스·순서 재배열 인덱스도 자연스럽게 처리할 수 있어요. 아래 예제는 Decomposable 객체를 분해한 뒤 그 요소들을 인덱스 순서대로 재조합해요.
#include <tuple>
template<std::size_t... Indices, typename Decomposable>
constexpr auto splice(Decomposable d)
{
auto [...elems] = d;
return std::make_tuple(elems...[Indices]...);
}
struct Point
{
int x;
int y;
int z;
};
int main()
{
constexpr Point p{.x = 1, .y = 4, .z = 3};
static_assert(splice<2, 1, 0>(p) == std::make_tuple(3, 4, 1));
static_assert(splice<1, 1, 0, 0>(p) == std::make_tuple(4, 4, 1, 1));
}