パックインデックスの制約を踏まえて使いたい
- 可変長テンプレートの N 番目を取りたいだけなのに、
std::tuple_elementや再帰テンプレートで回りくどくなる - C++26 の新機能を、自分のコンパイラで試せるのか分からない
- Visual Studio で
/std:c++latestを付けても、パックインデックスのコードが通らない
対象読者: C++20 以降のテンプレートを業務で書いている方。
前提知識: パラメータパックと畳み込み式(fold expression)。
C++26 は 2026年10月時点でまだ ISO 規格として発行されていません(DIS 投票の段階です)。本記事で扱うパックインデックスは GCC 15 / Clang 19 で実装済みですが、MSVC は Visual Studio 2026 でも未対応です。最終規格で仕様が変わる可能性があります。
結論
パックインデックスはパックの N 番目を パック名...[N] と直接書けるようにする構文です。再帰テンプレートも std::tuple も要らなくなります。
// パックインデックスの基本形(式パックの索引)
#include <cstdio>
template <typename... Ts>
auto first(Ts... ts) {
return ts...[0];
}
template <typename... Ts>
auto last(Ts... ts) {
return ts...[sizeof...(ts) - 1];
}
int main() {
std::printf("first=%d last=%d\n", first(1, 2, 3, 4, 5), last(1, 2, 3, 4, 5));
}
// 実行結果
// first=1 last=5
添字は定数式である必要があり、範囲外や空のパックはコンパイルエラーになります。対応コンパイラは GCC 15 以上と Clang 19 以上、MSVC は未対応です。
パックインデックスとは
パックインデックスはパラメータパックから N 番目の要素を直接取り出す構文です。WG21 の提案文書 P2662R3 で C++26 に入りました。
これまでは std::tuple に詰め替えるか、再帰テンプレートを書くしかありませんでした。再帰版は添字ごとにインスタンス化が増えるため、パック全体を走査するとインスタンス化の回数が要素数の2乗に比例して増えます。
その差は書き味だけでなく、コンパイル時間にも影響します。1つの構文が増えるだけで、テンプレートのメタプログラミングの書き方が変わります。
| やりたいこと | C++23 まで | C++26 |
|---|---|---|
| 型を取り出す | typename std::tuple_element_t<N, std::tuple<Ts…>> | Ts…[N] |
| 値を受け取る | std::get<N>(std::forward_as_tuple(…)) | ts…[N] |
| 添字の列で並べ替える | index_sequence と get の組み合わせ | ts…[Is]… |
添字に使えるのはコンパイル時の定数だけです。パックを実行時のコンテナに変える機能ではありません。
構文は3つある
パックインデックスには、式・型・添字の列という3つの使い方があります。
式パックの索引
関数の引数パックから、1つの値を取り出します。
// パックインデックスの基本形(式パックの索引)
#include <cstdio>
template <typename... Ts>
auto first(Ts... ts) {
return ts...[0];
}
template <typename... Ts>
auto last(Ts... ts) {
return ts...[sizeof...(ts) - 1];
}
int main() {
std::printf("first=%d last=%d\n", first(1, 2, 3, 4, 5), last(1, 2, 3, 4, 5));
std::printf("__cpp_pack_indexing=%ldL\n", static_cast<long>(__cpp_pack_indexing));
}
// 実行結果
// first=1 last=5
// __cpp_pack_indexing=202311L
型パックの索引
型を取り出すときは、エイリアステンプレートの右辺に置けます。
// 型パックの索引と decltype
#include <cstdio>
#include <string>
#include <type_traits>
#include <utility>
template <std::size_t I, typename... Ts>
using nth_type_t = Ts...[I];
template <std::size_t I, typename... Ts>
constexpr decltype(auto) get_nth(Ts&&... vs) {
return std::forward<Ts...[I]>(vs...[I]);
}
int main() {
using T0 = nth_type_t<0, int, double, std::string>;
using T2 = nth_type_t<2, int, double, std::string>;
static_assert(std::is_same_v<T0, int>);
static_assert(std::is_same_v<T2, std::string>);
std::string s = "hello";
auto&& r = get_nth<2>(1, 2.5, s);
static_assert(std::is_same_v<decltype(r), std::string&>);
}
// 実行結果
// type_pack ok, kind=std::string
Ts...[I] は型としても値としても書けます。転送するときは、型と値の両方に同じ添字を当てるのがポイントです。
添字の列で展開する
ts...[Is]... と書くと、添字のパック側で展開されます。並べ替えも繰り返しも自由です。
// インデックスパックでの展開(並べ替えと繰り返し)
#include <cstdio>
#include <tuple>
template <std::size_t... Is, typename... Ts>
constexpr auto select(Ts&&... vs) {
return std::make_tuple(vs...[Is]...);
}
int main() {
constexpr auto reversed = select<2, 1, 0>(10, 20, 30);
static_assert(reversed == std::tuple{30, 20, 10});
constexpr auto repeated = select<1, 1, 0, 0>(10, 20, 30);
static_assert(repeated == std::tuple{20, 20, 10, 10});
std::printf("reversed=(%d,%d,%d) repeated=(%d,%d,%d,%d)\n",
std::get<0>(reversed), std::get<1>(reversed), std::get<2>(reversed),
std::get<0>(repeated), std::get<1>(repeated), std::get<2>(repeated),
std::get<3>(repeated));
}
// 実行結果
// reversed=(30,20,10) repeated=(20,20,10,10)
展開されるのは Is であって vs ではありません。ここを取り違えると、要素数の合わないタプルができます。
制約
守るべきルールは4つです。どれもコンパイル時に判定されます。
- 添字は定数式でなければならない(実行時の変数は使えない)
- 添字はパックの範囲内でなければならない
- 空のパックは索引できない
- テンプレートのパックは索引できない(P3670 として C++29 で検討中)
実際に踏むのは、定数式でない添字と範囲外の2つです。
// 制約1: 添字は定数式でなければならない
#include <cstdio>
template <typename... Ts>
auto at(std::size_t i, Ts... ts) {
return ts...[i];
}
int main() {
std::printf("%d\n", at(1, 10, 20, 30));
}
// 実行結果(g++ -std=c++26)
// error: 'i' is not a constant expression
// error: 'i' is not a constant expression [-Wtemplate-body]
// error: pack index is not an integral constant
// 制約2: 添字はパックの範囲内でなければならない
#include <cstdio>
template <typename... Ts>
auto at(Ts... ts) {
return ts...[5];
}
int main() {
std::printf("%d\n", at(10, 20, 30));
}
// 実行結果(g++ -std=c++26)
// error: pack index '5' is out of range for pack of length '3'
// 制約3: 空のパックは索引できない
#include <cstdio>
template <typename... Ts>
auto first_or_zero(Ts... ts) {
return ts...[0];
}
int main() {
std::printf("%d\n", first_or_zero());
}
// 実行結果(g++ -std=c++26)
// error: cannot index an empty pack
エラーメッセージは「何番目が範囲外か」まで出ます。テンプレートのメタプログラミングで一番つらかったデバッグも、ここでは素直です。
エッジケース
実務で引っかかりやすい3つを挙げます。
非推論コンテキストを作れる
関数の引数に Ts...[0] と書くと、その引数はテンプレート引数の推論に使われなくなります。型を明示したいときの逃げ道として便利です。
// 非推論コンテキストを作る
#include <cstdio>
#include <string_view>
template <typename...>
struct type_seq {};
template <typename... Ts>
auto f(Ts...[0] arg, type_seq<Ts...>) {
return arg;
}
int main() {
auto a = f("Hello", type_seq<std::string_view>{});
std::printf("a.size()=%zu\n", a.size());
}
// 実行結果
// a.size()=5
添字付きの無名配列パラメータパックの意味が変わる
C++23 まで T...[1] は「サイズ1の無名配列パラメータパック」でした。C++26 では同じ字面がパックインデックスとして解釈されます。
// 旧文法: 添字付きの無名配列パラメータパック
template <typename... Ts>
void f(Ts... [1]);
int main() {
f<char, bool>(nullptr, nullptr);
}
// 実行結果(g++ -std=c++26)
// error: no matching function for call to 'f<char, bool>(std::nullptr_t, std::nullptr_t)'
// error: candidate expects 1 argument, 2 provided
実測では GCC 16 の -std=c++23 でも新しい解釈になり、-Wc++26-extensions の警告が出ました。古い意味に戻すなら、パラメータパックに名前を付けるか、ポインタ型のパックに書き換えます。
構造化束縛が導入したパックを索引する
P1061 で、構造化束縛がパックを導入できるようになりました。メンバをその場で取り出して並べ替えられます。
// 構造化束縛が導入したパックを、その場で索引する
#include <cstdio>
#include <tuple>
struct Point {
int x;
int y;
int z;
};
template <std::size_t... Indices, typename Decomposable>
constexpr auto splice(Decomposable d) {
auto [...elems] = d;
return std::make_tuple(elems...[Indices]...);
}
int main() {
constexpr Point p{.x = 1, .y = 4, .z = 3};
constexpr auto s = splice<2, 1, 0>(p);
static_assert(s == std::make_tuple(3, 4, 1));
std::printf("splice<2,1,0>(1,4,3) = (%d,%d,%d)\n",
std::get<0>(s), std::get<1>(s), std::get<2>(s));
}
// 実行結果
// splice<2,1,0>(1,4,3) = (3,4,1)
実測: 再帰テンプレートとの差
同じ仕事をする2つのコードを用意して、コンパイル時間を測りました。400個の型パックから全要素のサイズを合計する処理です。
| 実装 | 1回目 | 2回目 | 3回目 |
|---|---|---|---|
| 再帰テンプレート(C++23 までの書き方) | 2680 ms | 2689 ms | 2709 ms |
| パックインデックス | 147 ms | 143 ms | 142 ms |
どちらも -O0 で、同じソースを3回ずつビルドした実測値です。両方の実行ファイルが同じ合計値を返すことを確認してから測っているので、比較は成立しています。
再帰版は添字ごとにインスタンス化が増えるため、パックが大きくなるほど差が開きます。
// 旧: 再帰で N 番目の型を取り出す(添字ごとに1段ずつ増える)
template <std::size_t I, typename T0, typename... Rest>
struct nth { using type = typename nth<I - 1, Rest...>::type; };
// 新: パックインデックス(1回で決まる)
template <std::size_t I, typename... Ts>
using nth = Ts...[I];
// 実行結果
// old: total=6472
// new: total=6472
ビルド時間が気になる大きなテンプレートを書いているなら、ここは効果が出やすい場所です。
つまずきポイント
実際に踏んだ順に3つ挙げます。
- Visual Studio ではコンパイルできない。MSVC は
/std:c++latestでも構文エラーになる - g++ は
-std=c++26(または c++2c)を明示しないと使えない - 機能の有無は
__cpp_pack_indexingマクロで判定する(値は 202311L)
いちばん大きいのは1つ目です。Windows 開発の主戦場である Visual Studio 2026 の MSVC 14.50 で試すと、こうなります。
// MSVC で同じコードが通るか
#include <cstdio>
template <typename... Ts>
auto first(Ts... ts) {
return ts...[0];
}
int main() {
std::printf("%d\n", first(1, 2, 3));
}
// 実行結果(cl /std:c++latest)
// error C2760: 構文エラー: '...' は予期しないエラーでした。';' が必要です
// error C3878: 構文エラー: 'jump-statement' の後に予期しないトークン '...' があります
// error C3520: 'ts': パラメーター パックはこのコンテキストで展開する必要があります
MSVC では今のところ、C++26 のパックインデックスを使うコードは Windows でビルドできません。WSL や MinGW など、GCC / Clang が使える経路を用意しておく必要があります。
まとめ
できることと、できないことを整理します。
| 項目 | パックインデックス | 従来の方法 |
|---|---|---|
| N 番目の型 | Ts…[N] と直接書ける | tuple_element か再帰テンプレート |
| N 番目の値 | ts…[N] と直接書ける | std::get に詰め替えが必要 |
| 添字の列での並べ替え | ts…[Is]… で書ける | index_sequence と get の組み合わせ |
| 実行時の添字 | 使えない(定数式のみ) | 同じく使えない |
| Visual Studio でのビルド | 未対応 | 対応済み |
C++26 の目玉のひとつですが、対応コンパイラが GCC と Clang に限られます。まずは検証環境で試して、書き味とコンパイル時間を確かめてみてください。
検証環境
| OS | Windows 11 (build 10.0.26200) |
|---|---|
| CPU | AMD Ryzen 9 PRO 8945HS w/ Radeon 780M Graphics |
| メモリ | 28GB |
| 言語/ツール | MinGW-w64 GCC 16.2.0(winlibs の公式ポータブル配布を一時ディレクトリに展開) / MSVC 14.50.35717(Visual Studio 2026 Community) |
| 使用コマンド | g++ -std=c++26 でパックインデックスのコードを実行 / g++ -O0 -c で再帰版とパックインデックス版のコンパイル時間を計測 / cl /std:c++latest で MSVC が未対応であることを再現 |
| 測定日 | 2026-10-09 |
上記の環境で実際に実行して確認した結果です。環境が異なる場合は挙動が変わることがあります。

参考
- cppreference — Pack indexing (since C++26)
- P2662R3 — Pack Indexing(提案文書)
- cppreference — Compiler support for C++26
- GCC — C++ Standards Support
- cppreference — Feature testing
- P1061R10 — Structured bindings can introduce a pack
- P3670R4 — Pack Indexing for Template Names
【広告】お名前.comならドメイン取得が格安。![]()
あわせて読みたい
- CMake 4「Compatibility with CMake < 3.5」エラーの直し方
- 【C++】Visual Studio 2026 のC++新機能 — Segment Heap と C++23/26
- 【C++】C++26 Static Reflection 完全ガイド — コード自動生成
- 【Python】flake8 + black + isort から Ruff 0.17 へ移行したログ — 実際に壊れた4箇所
【広告】このサイトはConoHa WINGで運営しています。安定した高速サーバーで快適にブログを書けています。いつもありがとう!!![]()

コメント