Pack Indexing (C++26)
C++26 pack indexing (P2662): access individual elements of parameter packs with pack...[N] — no more recursive TMP or std::tuple_element to grab the Nth type or value from a variadic pack.
Pack Indexing (P2662)since C++26Pack indexing lets you write pack...[N] to access the Nth element of a template parameter pack or function parameter pack directly — replacing recursive std::tuple_element-style metaprogramming with clean subscript syntax.
The Problem It Solves
Before C++26, accessing the Nth element of a pack required either recursive TMP or std::tuple as an intermediary:
// Pre-C++26: cumbersome tuple_element trick
template<size_t N, typename... Ts>
using nth_type = std::tuple_element_t<N, std::tuple<Ts...>>;
// Pre-C++26: recursive instantiation
template<size_t N, typename First, typename... Rest>
struct nth {
using type = typename nth<N-1, Rest...>::type;
};
template<typename First, typename... Rest>
struct nth<0, First, Rest...> {
using type = First;
};Both approaches create unnecessary template instantiations and compile-time overhead.
Syntax
// Type pack indexing — access Nth type
template<typename... Ts>
using third_type = Ts...[2]; // zero-indexed
// Value pack indexing — access Nth value
template<auto... Vs>
constexpr auto third_value = Vs...[2];
// In function parameters
template<typename... Ts>
void get_first(Ts... args) {
auto first = args...[0]; // first element
}The index N must be a constant expression in [0, sizeof...(pack)).
Practical Examples
Type Selection
template<size_t N, typename... Ts>
using at_t = Ts...[N]; // clean, no tuple needed
static_assert(std::is_same_v<at_t<1, int, float, double>, float>);
static_assert(std::is_same_v<at_t<0, char, bool>, char>);Value Pack Access
template<auto... Ns>
constexpr auto sum_first_two() {
return Ns...[0] + Ns...[1];
}
static_assert(sum_first_two<10, 20, 30>() == 30);Function Argument Selection
// Return the last argument
template<typename... Ts>
auto last(Ts... args) {
return args...[sizeof...(Ts) - 1];
}
static_assert(last(1, 2, 3, 4) == 4);
static_assert(last("hello", "world") == std::string_view{"world"});Building Type-Safe Heterogeneous Access
#include <variant>
template<typename... Ts>
class TypeList {
public:
// Access type at position N
template<size_t N>
using type_at = Ts...[N];
// Construct variant of all types
using variant_type = std::variant<Ts...>;
// Check if Nth type matches
template<size_t N, typename T>
static constexpr bool is_at = std::is_same_v<type_at<N>, T>;
};
using MyTypes = TypeList<int, float, std::string>;
static_assert(std::is_same_v<MyTypes::type_at<2>, std::string>);
static_assert(MyTypes::is_at<0, int>);Comparison: Before and After
// Before C++26 — requires external trait or recursive template
template<typename... Ts>
struct MyTuple {
std::tuple<Ts...> data;
template<size_t N>
auto get() { return std::get<N>(data); }
// Getting the type required std::tuple_element:
template<size_t N>
using value_type = std::tuple_element_t<N, std::tuple<Ts...>>;
};
// After C++26 — direct pack subscript
template<typename... Ts>
struct MyTuple {
std::tuple<Ts...> data;
template<size_t N>
auto get() { return std::get<N>(data); }
// Now just index the pack directly:
template<size_t N>
using value_type = Ts...[N]; // No tuple_element boilerplate
};With Fold Expressions
Pack indexing composes well with fold expressions and expansion statements:
// Check if any type in pack is same as first
template<typename... Ts>
constexpr bool any_match_first = (std::is_same_v<Ts...[0], Ts> || ...);
// Swap first and last types
template<typename... Ts>
using swap_ends = TypeList<Ts...[sizeof...(Ts)-1], Ts...[1]..., Ts...[0]>;
// Note: middle elements need expansion (simplified example)Interaction with auto Parameter Packs (C++20)
Pack indexing works with abbreviated function template packs too:
// C++20 abbreviated templates + C++26 pack indexing
auto first_arg(auto... args) {
return args...[0];
}
auto last_arg(auto... args) {
return args...[sizeof...(args) - 1];
}
static_assert(first_arg(10, 20, 30) == 10);
static_assert(last_arg(10, 20, 30) == 30);Key Rules
- Index must be a constant expression — no runtime indexing of packs (that's what
std::tupleand arrays are for) - Zero-indexed: first element is
pack...[0] - Out-of-bounds index is a hard compile error (not UB)
- Works for both type packs (
typename... Ts) and value packs (auto... Vs,int... Ns) - The
...is part of the syntax —pack...[N], notpack[N] - A pack of size 0 cannot be indexed (compile error); check
sizeof...(pack) > Nfirst if needed - Only plain id-expressions can be indexed —
(Vals * 3)...[I]is an error; writeVals...[I] * 3instead - Cannot index a pack of template template parameters (
template<typename...> typename... Temps)
Compiler Support
Pack indexing (P2662R3) is supported as of these compiler versions (use -std=c++26 or -std=c++2c):
| Compiler | Version | Flag |
|---|---|---|
| GCC | 15+ | -std=c++26 |
| Clang | 19+ | -std=c++26 |
| MSVC | not yet | — |
| Apple Clang | not yet | — |
| EDG eccp | not yet | — |
Source: cppreference compiler support table (P2662R3 row).
Note: Ubuntu's system clang++ is often Clang 18 even after installing clang-20 via apt — always invoke clang++-20 (or whichever version) explicitly to get pack indexing support.
The feature test macro is __cpp_pack_indexing.
#if __cpp_pack_indexing >= 202311L
// pack indexing available
template<std::size_t N, typename... Ts>
using at_t = Ts...[N];
#else
// fallback
template<std::size_t N, typename... Ts>
using at_t = std::tuple_element_t<N, std::tuple<Ts...>>;
#endifPractical Pattern: Type Dispatch Without tuple_element
Before C++26, writing a get_nth that returns the Nth argument by reference required either packing into a tuple (which forces copies) or a recursive-template accumulator. Pack indexing makes it a one-liner:
// C++26: get the Nth argument by forwarded reference
template<std::size_t N, typename... Args>
constexpr decltype(auto) get_nth(Args&&... args) noexcept {
// Index both the type pack and the value pack in tandem
return std::forward<Args...[N]>(args...[N]);
}
static_assert(get_nth<0>(10, 20, 30) == 10);
static_assert(get_nth<2>(10, 20, 30) == 30);
// Works with mixed types too
auto& ref = get_nth<1>(std::string{"hello"}, 42, 3.14);
// ref is int&& (forwarded rvalue ref)Note the correct std::forward idiom: you must index both the type pack (Args...[N]) and the value pack (args...[N]) separately — std::forward<Args>(args)...[N] is a compile error because std::forward<Args>(args) is not a bare id-expression.
Interaction with std::integer_sequence
Pack indexing composes naturally with index sequences to express permutations, projections, and slice operations over packs.
Reorder / Permute a Pack
// Select elements at arbitrary compile-time positions
template<std::size_t... Is, typename... Ts>
auto select_types(std::index_sequence<Is...>)
-> std::tuple<Ts...[Is]...>; // expand: pick type at each index
// E.g. reverse a 3-element pack
using Reversed = decltype(
select_types<int, float, double>(std::index_sequence<2, 1, 0>{})
);
static_assert(std::is_same_v<Reversed, std::tuple<double, float, int>>);Drop the First Element ("tail")
template<typename First, typename... Rest>
using tail_t = std::tuple<Rest...>; // simple case — but generalising:
// Drop first N elements using offset index_sequence
template<std::size_t Drop, typename... Ts,
std::size_t... Is>
auto drop_front_impl(std::index_sequence<Is...>)
-> std::tuple<Ts...[Is + Drop]...>;
template<std::size_t Drop, typename... Ts>
using drop_front_t = decltype(
drop_front_impl<Drop, Ts...>(
std::make_index_sequence<sizeof...(Ts) - Drop>{}));
static_assert(std::is_same_v<
drop_front_t<1, int, float, double>,
std::tuple<float, double>>);Reverse a Pack
template<typename... Ts, std::size_t... Is>
auto reverse_impl(std::index_sequence<Is...>)
-> std::tuple<Ts...[sizeof...(Ts) - 1 - Is]...>;
template<typename... Ts>
using reverse_t = decltype(
reverse_impl<Ts...>(std::make_index_sequence<sizeof...(Ts)>{}));
static_assert(std::is_same_v<
reverse_t<int, float, double>,
std::tuple<double, float, int>>);Compile-Time Index Generation: front_n_t
Taking the first N types from a pack previously required a recursive partial specialisation. With pack indexing and index_sequence it collapses to an alias:
template<std::size_t N, typename... Ts, std::size_t... Is>
auto front_n_impl(std::index_sequence<Is...>)
-> std::tuple<Ts...[Is]...>;
// Public alias: first N types of the pack as a tuple
template<std::size_t N, typename... Ts>
using front_n_t = decltype(
front_n_impl<N, Ts...>(std::make_index_sequence<N>{}));
static_assert(std::is_same_v<front_n_t<2, int, float, double>,
std::tuple<int, float>>);
static_assert(std::is_same_v<front_n_t<1, char, bool, short>,
std::tuple<char>>);This pattern works for any pack projection: build an index_sequence that describes which positions you want, then expand Ts...[Is]....
Runtime Value Access: Compile-Time Dispatch Bridge
Pack indexing is purely compile-time — the index must be a constant expression. To pick one of N values at runtime you need a dispatch layer that converts the runtime integer into a template argument:
// Compile-time case: direct pack indexing
template<std::size_t I, typename... Ts>
constexpr auto pick_ct(Ts... args) {
return args...[I];
}
// Runtime dispatch: recurse/switch over compile-time specialisations
template<typename... Ts>
auto pick_rt(std::size_t i, Ts... args) {
// Build a table of function pointers, one per index
using FnPtr = std::common_type_t<Ts...> (*)();
// Expand a pack of lambdas capturing each element
std::array fns{
+[&]() -> std::common_type_t<Ts...> { return args...[0]; },
// ... generated via index_sequence expansion:
};
return fns[i]();
}
// Cleaner version using index_sequence expansion
template<typename... Ts, std::size_t... Is>
auto pick_rt_impl(std::size_t i, std::index_sequence<Is...>, Ts... args) {
using R = std::common_type_t<Ts...>;
std::array<R, sizeof...(Ts)> arr{ static_cast<R>(args...[Is])... };
return arr[i]; // runtime lookup into compile-time-filled array
}
template<typename... Ts>
auto pick_rt(std::size_t i, Ts... args) {
return pick_rt_impl(i, std::make_index_sequence<sizeof...(Ts)>{},
args...);
}
// Usage
auto val = pick_rt(1, 10, 20, 30); // returns 20 at runtimeThe key insight: pack indexing fills the array at compile time (each args...[Is] is a constant index), then you do a normal runtime array lookup. This is strictly more efficient than a chain of if/else or a recursive template at runtime.
Pack Indexing in Concepts and requires Expressions
Pack indexing is a constant expression, so it is valid anywhere a type or value is needed inside a concept. This lets you constrain individual elements of a pack without expanding the whole pack or introducing an auxiliary trait.
// C++26
#include <concepts>
#include <type_traits>
// Require that the first type in the pack is integral
template<typename... Ts>
concept FirstIsIntegral = std::integral<Ts...[0]>;
template<FirstIsIntegral... Ts>
void process(Ts... args) { /* ... */ }
process(1, 2.0, true); // OK — first type (int) is integral
// process(1.0, 2, 3); // ill-formed — first type (double) is not integralYou can also use pack indexing directly inside an ad-hoc requires clause on a function template. This constrains a specific positional argument without having to name every type in the pack.
// C++26
// Require that the second argument (index 1) is convertible to std::string_view
template<typename... Ts>
requires (sizeof...(Ts) >= 2) &&
std::convertible_to<Ts...[1], std::string_view>
void log_with_tag(Ts... args) {
std::string_view tag = args...[1];
// ... use tag
}
log_with_tag(42, "startup", 3.14); // OK — args...[1] is const char*, converts to string_view
// log_with_tag(42, 99); // ill-formed — int is not convertible to string_viewComposing pack indexing with requires is especially useful when building heterogeneous pipelines whose first or last slot must satisfy a fixed contract while the remaining positions remain unconstrained.
Out-of-Range Indices: Compile Errors and Better Diagnostics
An out-of-bounds pack index is a hard compile error — there is no undefined behaviour, no silent truncation, and no SFINAE fallback. The compiler reports the error at the point of instantiation, typically with a message naming both the invalid index and the actual pack size.
// C++26
template<typename... Ts>
using bad_alias = Ts...[3]; // pack has fewer than 4 elements
using X = bad_alias<int, float>;
// error: pack index 3 is out of range for pack of size 2Without additional effort the error appears deep inside an instantiation chain, making it hard to pinpoint the call site. Providing a static_assert with a human-readable message before the index expression surfaces the mistake at the right abstraction level.
// C++26
template<std::size_t N, typename... Ts>
struct safe_at {
static_assert(N < sizeof...(Ts),
"safe_at<N, Ts...>: index N is out of range — "
"the pack has fewer than N+1 elements.");
using type = Ts...[N];
};
template<std::size_t N, typename... Ts>
using safe_at_t = typename safe_at<N, Ts...>::type;
// safe_at_t<5, int, float> now produces:
// static assertion failed: safe_at<N, Ts...>: index N is out of range ...
// instead of a raw "pack index 5 is out of range for pack of size 2"Similarly, for value packs inside a consteval or constexpr function, you can call static_assert on the index before using it, so the diagnostic is issued in the function body rather than an opaque library header. One extra line of defence per public API pays for itself the first time a caller accidentally transposes arguments.
Replacing std::get and std::tuple_element
Pack indexing directly supersedes the std::tuple_element_t / std::get<N> pattern for type and value retrieval whenever the pack itself is in scope. The two mechanisms are not identical — std::get operates on an already-constructed tuple object at runtime, whereas pack indexing operates on the pack of types or values at compile time — but they solve overlapping problems and pack indexing is often the simpler tool.
// C++26
// Old idiom: wrap pack in a tuple just to extract a type
template<std::size_t N, typename... Ts>
using nth_type_old = std::tuple_element_t<N, std::tuple<Ts...>>;
// New idiom: direct subscript — no tuple instantiation at all
template<std::size_t N, typename... Ts>
using nth_type = Ts...[N];
static_assert(std::is_same_v<nth_type<2, char, bool, short>, short>);
static_assert(std::is_same_v<nth_type_old<2, char, bool, short>, short>);For extracting a value from a function parameter pack std::get<N>(tuple) requires you to build the tuple first, paying for construction and (potentially) copies; pack indexing avoids the object entirely.
// C++26
// Old: must construct a tuple to then get<N> out of it
template<std::size_t N, typename... Ts>
auto nth_value_old(Ts... args) {
return std::get<N>(std::make_tuple(args...)); // copy/move each arg into tuple
}
// New: index the pack directly — zero overhead, no intermediate object
template<std::size_t N, typename... Ts>
constexpr decltype(auto) nth_value(Ts&&... args) {
return std::forward<Ts...[N]>(args...[N]);
}
static_assert(nth_value<1>(10, 20, 30) == 20);std::get and std::tuple_element remain the right tools when you already hold a std::tuple variable and want to access its fields, or when you need structured binding decomposition. Pack indexing is the right tool when the pack itself is what you have — in template parameter lists, abbreviated function templates, and requires expressions — and you want to avoid the indirection of constructing a tuple purely as an extraction vehicle.