【C++26】パックインデックス深掘り — 制約・エッジケースと MSVC で使えない理由

C++26 のパックインデックス(可変長テンプレートの N 番目を取り出す構文)を抽象的に表現したアイキャッチ画像
目次

パックインデックスの制約を踏まえて使いたい

  • 可変長テンプレートの 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 ms2689 ms2709 ms
パックインデックス147 ms143 ms142 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 に限られます。まずは検証環境で試して、書き味とコンパイル時間を確かめてみてください。

検証環境

OSWindows 11 (build 10.0.26200)
CPUAMD 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

上記の環境で実際に実行して確認した結果です。環境が異なる場合は挙動が変わることがあります。

C++26 パックインデックスの実測ログ。GCC 16.2.0 での実行結果(first/last・型パック・並べ替え・構造化束縛・非推論コンテキスト)、制約違反3種の実エラーメッセージ、無名配列パックの解釈変更、400型パックでのコンパイル時間(再帰版 約2.7秒 / パックインデックス版 約0.14秒)、そして MSVC 14.50 では構文エラーになることまでを1枚にまとめたもの
C++26 パックインデックスの実測ログ。GCC 16.2.0 での実行結果(first/last・型パック・並べ替え・構造化束縛・非推論コンテキスト)、制約違反3種の実エラーメッセージ、無名配列パックの解釈変更、400型パックでのコンパイル時間(再帰版 約2.7秒 / パックインデックス版 約0.14秒)、そして MSVC 14.50 では構文エラーになることまでを1枚にまとめたもの

参考

【広告】お名前.comならドメイン取得が格安。

あわせて読みたい

【広告】このサイトはConoHa WINGで運営しています。安定した高速サーバーで快適にブログを書けています。いつもありがとう!!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次