yohhoyの日記

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

std::mdspanコンストラクタとCTAD

C++2b(C++23)標準ライブラリに追加される多次元ビューstd::mdspanクラステンプレート(→id:yohhoy:20230303)の、コンストラクタと推論ガイド(deduction guide)による実引数クラステンプレート推論(CTAD; Class Template Argument Deduction)のあれこれ。

mdspan型における次元情報std::extents<I, En...>では、各次元の要素数(En)はコンパイル時定数/実行時動的指定(std::dynamic_extent)のいずれかとなる。ほとんどのCTAD*1では動的サイズ(std::dextents<I, R>)に型推論される。

デフォルトコンストラク

動的要素数次元を含む場合のみデフォルト構築可能。

// 2次元ビュー/動的サイズ
using Matrix = std::mdspan<double, std::dextents<size_t, 2>>;
Matrix m1;  // OK: 空(サイズ0x0)の2次元ビュー

// 2次元ビュー/動的+固定サイズNx3
using ViewNx3 = std::mdspan<double, std::extents<size_t, std::dynamic_extent, 3>>;
ViewNx3 m2;  // OK: 空(サイズ0x3)の2次元ビュー

// 2次元ビュー/固定サイズ3x3
using Mat3x3 = std::mdspan<double, std::extents<size_t, 3, 3>>;
Mat3x3 m3;  // NG: コンパイルエラー(ill-formed)

素数リスト指定コンストラク

全次元の要素数リストまたは動的要素数の次元に対する要素数リストを、整数リスト/std::array<I, N>std::span<I, N>にて指定する。CTAD利用時のインデクス型(mdspan::index_type)は推論ガイドによりsize_tとなる。

// 整数リストによる要素数指定
double *ptr = /*...*/;

// 2次元ビュー/動的サイズ2x3
using Matrix = std::mdspan<double, std::dextents<size_t, 2>>;
Matrix      m1{ptr, 2, 3};  // OK
std::mdspan m2{ptr, 2, 3};  // OK: CTAD

// 2次元ビュー/固定サイズ2x3
using Mat2x3 = std::mdspan<double, std::extents<size_t, 2, 3>>;
Mat2x3 m3a{ptr};        // OK
Mat2x3 m3c{ptr, 2};     // NG: ill-formed
Mat2x3 m3b{ptr, 2, 3};  // OK
Mat2x3 m3d{ptr, 0, 42};  // NG: 未定義動作(UB)
// 固定サイズ次元におけるコンストラクタ引数の不一致は、
// std::extents(OtherIndexTypes...)コンストラクタの
// 事前条件(Preconditions)違反のためUBを引き起こす。
// std::array<I, N>による要素数指定
double *ptr = /*...*/;
std::array<int, 2> exts = {2, 3};

// 2次元ビュー/動的サイズ2x3
using Matrix = std::mdspan<double, std::dextents<size_t, 2>>;
Matrix      m1{ptr, exts};  // OK
std::mdspan m2{ptr, exts};  // OK: CTAD
// std::span<I, N>による要素数指定
double *ptr = /*...*/;
std::vector<int> exts_vec = {2, 3};
std::span<int, 2> exts{exts_vec};

// 2次元ビュー/動的サイズ2x3
using Matrix = std::mdspan<double, std::dextents<size_t, 2>>;
Matrix      m1{ptr, exts};  // OK
std::mdspan m2{ptr, exts};  // OK: CTAD

std::span<int, std::dynamic_extent> exts2{&exts_list[0], 2};
Matrix      m3{ptr, exts2};  // NG: 動的サイズspanは指定不可
std::mdspan m4{ptr, exts2};  // NG: 動的サイズspanは指定不可

固定サイズ/動的サイズ変換

固定サイズから動的サイズへは常に安全に変換可能。動的サイズから固定サイズへはサイズ情報が一致していれば変換可能。

double *ptr = /*...*/;
// 2次元ビュー型
using Matrix = std::mdspan<double, std::dextents<size_t, 2>>;
using Mat3x3 = std::mdspan<double, std::extents<size_t, 3, 3>>;
using Mat4x4 = std::mdspan<double, std::extents<size_t, 4, 4>>;

// 固定サイズ→動的サイズ変換
Mat3x3 src3x3{ptr};
Matrix m1{src3x3};  // OK

// 固定サイズ→固定サイズ変換
Mat4x4 m2{src3x3};  // NG: コンパイルエラー(ill-formed)

// 動的サイズ→固定サイズ変換
Matrix srcmat{ptr, 3, 3};
Mat3x3 m3{srcmat};  // OK
Mat4x4 m4{srcmat};  // NG: 未定義動作(UB)
// 動的サイズ→固定サイズ型変換におけるサイズ不一致は
// 事前条件(Preconditions)違反のためUBを引き起こす。

C配列型からの変換(CTAD)

1次元C配列型のみ直接変換がサポートされる。2次元以上のC配列型からのmdspan構築はコンパイルエラーとなるか、要素アクセス時に未定義動作を引き起こす。配列先頭要素ポインタからの変換は意図しない結果をもたらす。

// 1次元配列からの変換
double arr1d[100] = /*...*/;

// 1次元ビュー/固定サイズ100
using Array100 = std::mdspan<double, std::extents<size_t, 100>>;
Array100    m1{arr1d};  // OK
std::mdspan m2{arr1d};  // OK: CTAD

// 1次元ビュー/動的サイズ100
using ArrayN = std::mdspan<double, std::dextents<size_t, 1>>;
ArrayN      m3{arr1d, 100};  // OK
std::mdspan m4{arr1d, 100};  // OK: CTAD

// 1次元配列要素先頭ポインタから変換(CTAD)
std::mdspan m5{&arr1d[0], 100};  // OK: m4と等価
std::mdspan m6{&arr1d[0]};       // OK: 0次元ビュー(!)
// m6はstd::mdspan<double, std::extents<size_t>>型に推論される
// 2次元配列からの変換
double arr2d[4][4] = /*...*/;

// 2次元ビュー/サイズ4x4(?)
using Mat4x4 = std::mdspan<double, std::extents<size_t, 4, 4>>;
Mat4x4 m1{arr2d};        // NG: ill-formed
Mat4x4 m2{arr2d, 4, 4};  // NG: ill-formed
std::mdspan m3{arr2d};   // NG: ill-formed

std::mdspan m4{arr2d, 4, 4};  // NG
// プログラマの意図に反してm4は「要素型double[4]の2次元ビュー」
// std::mdspan<double[4], std::dextents<size_t, 2>>>型に推論され、
// double[4]型の4要素配列に対する2次元ビュー(4x4=16要素)を構築する。
// 例1: 要素代入 m4[0,0] = 3.14; は型不一致となるためill-formed
// 例2: インデクス位置[0,0~3]以外は範囲外のためUBを引き起こす

// 2次元配列先頭要素ポインタから変換(?)
Mat4x4      m5{&arr2d[0][0]};        // NG
std::mdspan m6{&arr2d[0][0], 4, 4};  // NG
// m6はstd::mdspan<double, std::dextents<size_t, 2>>>型に推論される。
// double[4][4]型をdouble[16]型として扱うため厳密にはUBとなる。
// 詳細は提案文書 P2554R0 を参照。

次元情報指定コンストラクタ(CTAD)

メモリ領域ポインタとstd::extentsstd::dextents*2を指定すると、任意の次元数(rank)と要素数(extents)をもつmdspan型を推論可能となる。CTADを用いるメリットは小さいが、第1引数のポインタ型から要素型(mdspan::element_type)が推論される。

double *ptr = /*...*/;

// 2次元ビュー/動的サイズ2x3
using Extents2D = std::dextents<size_t, 2>;
std::mdspan m1{ptr, Extents2D{2, 3}};  // OK

// 2次元ビュー/動的+固定サイズNx3
using ExtentsNx3 = std::extents<size_t, std::dynamic_extent, 3>;
std::mdspan m2{ptr, ExtentsNx3{2}};  // OK

// 2次元ビュー/固定サイズ2x3
using Extents2x3 = std::extents<size_t, 2, 3>;
std::mdspan m3{ptr, Extents2x3{}};  // OK

メモリレイアウト型変換

行優先(row major)std::layout_right/列優先(column major)std::layout_leftレイアウトから汎用ストライドstd::layout_strideレイアウトへは常に安全に変換可能。異種レイアウト間変換においては、マッピングのずらし幅(stride)に互換性があるケースに限って変換可能。
注:std::mdspanは実データに対する「ビュー」にすぎない。例えば行列の転置(transpose)をmdspan型変換のみで実現することはできない。

double *ptr = /*...*/;

// 2次元ビュー/固定サイズ3x3
using Extents3x3 = std::extents<size_t, 3, 3>;
using MatCpp = std::mdspan<double, Extents3x3 /*std::layout_right*/>;
using MatFortran = std::mdspan<double, Extents3x3, std::layout_left>;
using MatStrided = std::mdspan<double, Extents3x3, std::layout_stride>;

// C++/Fortran互換→汎用レイアウト変換
MatCpp     src_cpp{ptr};
MatFortran src_fortran{ptr};
MatStrided m1{src_cpp};      // OK
MatStrided m2{src_fortran};  // OK

// C++⇔Fortran互換レイアウト変換
MatFortran m3{src_cpp};      // NG
MatCpp     m4{src_fortran};  // NG

// 汎用レイアウト→C++/Fortran互換レイアウト変換
std::array<int, 2> strides{3, 1};
std::layout_stride::mapping<Extents3x3> mapping{{}, strides};
MatStrided src_stride{ptr, mapping};
MatCpp     m5{src_stride};  // OK
MatFortran m6{src_stride};  // NG: 未定義動作(UB)
// strides{3,1}のlayout_stride::mapping<Extents3x3>は
// layout_left::mapping<Extents3x3>と互換性がないため、
// 事前条件(Preconditions)違反となってUBを引き起こす。
double *ptr = /*...*/;

// 1次元ビュー/互換レイアウト(stride={1})
using Extents1D = std::extents<size_t, 10>;
using ArrayRow = std::mdspan<double, Extents1D, std::layout_right>;
using ArrayCol = std::mdspan<double, Extents1D, std::layout_left>;
ArrayRow src_row{ptr};
ArrayCol src_col{ptr};
ArrayCol m1{src_row};  // OK
ArrayRow m2{src_col};  // OK
// 次元数1ではlayout_right/layout_leftレイアウトは同一

その他のコンストラクタ(CTAD)

前掲以外のCTADとして、下記の実引数からのmdspan型推論がサポートされる。

  • レイアウトマップLayout::mapping
  • レイアウトマップLayout::mapping, 要素アクセサAccessor

関連URL

*1:C配列型からの推論ガイドでは固定サイズ1次元ビューへと推論される。第2引数に std::extents 型をとる推論ガイドでは、固定サイズや動的/固定サイズ混在の std::mdspan 型へと推論可能。

*2:std::dextents は std::extents クラステンプレートの別名(alias)として定義される。