yohhoyの日記

技術的メモをしていきたい日記

std::submdspan関数

C++2c(C++26)標準ライブラリに追加される多次元部分ビューstd::submdspanについて。

// <mdspan>ヘッダ
namespace std {
  template<
    class T, class E, class L, class A,
    class... SliceSpecifiers>
  constexpr auto submdspan(
    const mdspan<T, E, L, A>& src,
    SliceSpecifiers... slices) -> /*see below*/;
}

まとめ:

  • 多次元ビューstd::mdspan(→id:yohhoy:20230303)から部分ビューを取り出す(slice)関数。
  • 第2引数以降のスライス指定子リストにより、各次元のスライス方法(slicing)を指定する。
    • 単一インデクス:単一値。指定次元に対するインデクスを固定し、次元数(rank)を1つ削減する。*1
    • インデクス範囲:開始位置(begin)+終了位置(end)の組。std::pairや2要素std::tuple等により*2、指定次元に対するインデクス範囲を取り出す。
    • ストライド指定範囲:オフセット位置(offset)+要素数(extent)+ストライド(stride)の組。std::strided_sliceにより、指定次元に対してずらし幅指定したインデクス範囲を取り出す。
    • 全選択:タグ値std::full_extent。指定次元をそのまま取り出す。
  • ストライド指定範囲” 用のstrided_slice型は名前付き初期化(→id:yohhoy:20170820)をサポートする。
    • 類似機能を提供するPython/numpyやMatlabFortranと異なり、変換元における終了位置(end)ではなく変換元における要素数(extent)による指定を行う。
    • 例:strided_slice{.offset=1, .extent=10, .stride=3}またはstrided_slice{1, 10, 3}
    • 2024-12-08追記:提案文書P3355R1が採択され、stride==1なstrided_sliceはunit-stride sliceとして特別扱いされる。インデクス範囲指定や全選択(std::full_extent)と同様にレイアウトポリシー維持基準に影響する。
  • “インデクス範囲” および “ストライド指定範囲” で用いるインデクス値は、通常の値と整数定数の2種類をサポートする。*3
    • 戻り値型mdspan<T,E,L,A>における多次元インデクス型Eの各次元要素数に影響を与える。
    • 通常の値:整数リテラル(literal)*4を含む整数値。指定次元の要素数は実行時(std::dynamic_extent)に決定する。
    • 整数定数:std::integral_constant互換の定数値*5。指定次元の要素数コンパイル時に決定する。
  • レイアウトポリシーLC++標準ライブラリ提供メモリレイアウトのみサポートする。
    • 2024-08-05追記:std::layout_right_padded, std::layout_left_padded追加にともない変換ルールも複雑化している。更新版は id:yohhoy:20240805 参照。
    • std::layout_right:同ポリシー型を維持できる場合はlayout_rightを利用。それ以外はlayout_strideへ変換。
    • std::layout_left:同ポリシー型を維持できる場合はlayout_leftを利用。それ以外はlayout_strideへ変換。
    • std::layout_stridelayout_strideのまま。
    • ユーザ定義レイアウトポリシーをサポートするには、カスタマイズポイントsubmdspan_mapping関数を実装する。カスタマイズポイント実装は必須要件ではないが、汎用のフォールバック実装は提供されない。
  • 要素型TとアクセスポリシーAは原則維持される。*6

スライス指定の例

int a[15];  // {1, 2, ... 15}
std::ranges::iota(a, 1);

// 3x5要素の2次元ビュー
std::mdspan m0{a, std::extents<size_t, 3, 5>{}};
//  1  2  3  4  5
//  6  7  8  9 10
// 11 12 13 14 15

auto m1 = std::submdspan(m0, 1, std::full_extent);
// [6 7 8 9 10] (5要素1次元)
auto m2 = std::submdspan(m0, std::full_extent, 2);
// [3 8 13] (3要素1次元)
auto m3 = std::submdspan(m0, 1, 2);
// 8 (0次元)

// 2x3要素の2次元部分ビュー
auto m4d = std::submdspan(m0, std::pair{1,2}, std::tuple{1,3});
//  -  -  -  -  -
//  -  7  8  9  -
//  - 12 13 14  -
// Extents = std::dextents<size_t, 2>

// 2x2要素の2次元部分ビュー
auto m5d = std::submdspan(m0,
  std::strided_slice{.offset=0, .extent=3, .stride=2},
  std::strided_slice{.offset=1, .extent=4, .stride=3});
//  -  2  -  -  5
//  -  -  -  -  -
//  - 12  -  - 15
// Extents = std::dextents<size_t, 2>
template <int N>
constexpr auto Int = std::integral_constant<int, N>{};

// 2x3要素(静的要素数)の2次元部分ビュー
auto m4s = std::submdspan(m0,
  std::pair{Int<1>,Int<2>}, std::tuple{Int<1>,Int<3>});
//  -  -  -  -  -
//  -  7  8  9  -
//  - 12 13 14  -
// Extents = std::extents<size_t, 2, 3>

// 2x2要素(静的要素数)の2次元部分ビュー
auto m5s = std::submdspan(m0,
  std::strided_slice{.offset=0, .extent=Int<3>, .stride=Int<2>},
  std::strided_slice{.offset=1, .extent=Int<4>, .stride=Int<3>});
//  -  2  -  -  5
//  -  -  -  -  -
//  - 12  -  - 15
// Extents = std::extents<size_t, 2, 2>
// strided_slice::offset型はsubmdspan適用後の型に影響しない

レイアウトポリシー変換

int a[60] = /*...*/;

// 3x4x5要素の3次元ビュー(LayoutPolicy=layout_right)
std::mdspan m0{a, std::extents<size_t, 3, 4, 5>{}};

auto m1 = std::submdspan(m0, 1, std::full_extent, std::full_extent);
// LayoutPolicy = layout_right (4x5要素2次元)
auto m2 = std::submdspan(m0, 1, std::pair{1,2}, std::full_extent);
// LayoutPolicy = layout_right (2x5要素2次元)
auto m3 = std::submdspan(m0, 1, 0, 2);
// LayoutPolicy = layout_right (0次元)

auto m4 = std::submdspan(m0, std::full_extent, 0, std::full_extent);
// LayoutPolicy = layout_stride (3x5要素2次元)
// 変換後m4の strides[] = {20, 1}
// 3x4x5要素の3次元ビュー, layout_stride = {20, 1, 4}
using Exts3x4x5 = std::extents<size_t, 3, 4, 5>;
std::array strides = {20, 1, 4};
auto mapping = std::layout_stride::mapping{Exts3x4x5{}, strides};
std::mdspan m0s{a, mapping};

auto m5 = std::submdspan(m0s, 0, std::full_extent, 0);
// LayoutPolicy = layout_stride (4要素1次元)
assert(m5.mapping().stride(0) == 1);
// メモリレイアウト的にはlayout_right互換となる
// コンパイル時の型計算ではstridesアクセスできないため
// layout_strideからは常にlayout_strideへと変換される

// 変換コンストラクタによりlayout_rightへ明示変換可能
std::mdspan<int, std::extents<size_t, 4>> m5r{ m5 };
// LayoutPolicy = layout_right (4要素1次元)

関連URL

*1:1次元 mdspan からは 0次元 mdspan(→id:yohhoy:20230309)が生成される。

*2:2要素ペアとしてアクセス可能な型(index-pair-like)を広くサポートする。

*3:“単一インデクス” も整数定数をサポートするが、その効果は通常の値を指定したときと同じ。

*4:戻り値型の決定で利用されるため、std::integral_constant<int,N>::value のように値が型情報にエンコードされていないと、コンパイル時にその値(value)へアクセスできない。

*5:データメンバ value で定数値アクセス可能な型(integral-constant-like)を広くサポートする。

*6:厳密には submdspan 関数適用後 mdspan<T,E,L,A> の要素型 T やアクセスポリシー A は A::offset_policy 型に依存する。通常は A::offset_policy == A として定義されるため、テンプレートパラメータ T, A 型が変化するケースは稀。