팩 인덱싱

팩 인덱싱 (Pack indexing) (C++26부터)

템플릿에서 팩을 다루다 보면 "이 팩의 N번째 요소에 접근하고 싶다"는 요구가 생기는데, C++26 전에는 별도의 보조 템플릿(재귀나 std::tuple_element 같은)을 거쳐야 했어요. **팩 인덱싱(pack indexing)**은 팩에 첨자를 붙여 P...[I]처럼 바로 그 요소에 접근하는 문법이에요.

팩 인덱싱은 펼쳐지지 않은 팩 뒤에 줄임표와 첨자(subscript)가 오는 _팩 확장_이에요. 두 종류가 있어요: **팩 인덱싱 표현식(pack indexing expression)**과 팩 인덱싱 지정자(pack indexing specifier).

PP₀, 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)
  1. 팩 인덱싱 표현식
  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));
}

더 알아보기