[C++] mdspanでインターリーブレイアウトを扱う

mdspanお勉強のメモです。ここでのサンプルコードは全てkokkos/mdspanを用いて実行しています。標準のstd::mdspanに準拠して実装されているのでおそらく挙動は変わらないでしょう。

std::mdspanそのものについてはあまり解説しないので、std::mdspanの使い方などに関しては例えばこれらの記事などをご参照ください

インターリーブレイアウト

行列(配列)のインターリーブとは、同じサイズの複数の行列を1つの行列に詰め込むようなメモリレイアウトで、その際に同じ添字を持つ要素を連続的に配置するものです。

インターリーブレイアウトへの変換イメージ


(Interleave - File Exchange - MATLAB Centralより引用)

1次元(配列)の場合は、AoSに対してSoAと呼ばれるレイアウトのことです。

このようなレイアウトを取ることによる利点は、対応する添字要素をメモリ上で隣接させて配置することで局所性を高めメモリアクセスを効率化できる点です。

例えば、複数の行列を使用するある処理がそれら行列の対応する要素ごとに処理を行なっていくような場合、通常のレイアウトだと複数の行列間で対応する(同じ添字を持つ)要素はメモリ上で少なくとも行列サイズ分離れた位置に配置されています。それを読み込もうとすると異なる位置へのメモリアクセスが何度も発生し、1要素読み込むごとにキャッシュが無効化されるなど非効率となります。インターリーブレイアウトをとることで、1度のメモリアクセスで使いたい要素を一括でロードし、なおかつキャッシュにも乗りやすくなるなど効率化を図れます。

インターリーブレイアウトを用いる場合、行列のメモリ配置が複雑になり1つの行列にアクセスする際に工夫しなければなりません。

C++23からは多次元配列ビューであるstd::mdspanが追加されました。std::mdspanのデフォルトはC++の配列レイアウトを元にした行優先の多次元配列を扱いますが、std::mdspanはレイアウトをカスタムできるようになっており、それを利用するとインターリーブレイアウトを取り扱うことができるようになります。

この記事では、std::mdspanのレイアウトカスタマイズ機能を利用してインターリーブレイアウトをハンドルできるようにしながら、複雑に見えがちなstd::mdspanの内部構造に触れていきます。

なお、上記イメージのようにインターリーブレイアウトには行優先と列優先の2種類がありますが、ここではインターリーブはいつも行優先であるものとし列優先のレイアウトは扱いません。列優先でも考え方はそんなに変わらないはずですが。

mdspanのカスタマイズ

std::mdspanstd::mdspan<T, E, L, A>のように4つのテンプレートパラメータを受け取ります。ただし、実際使用する際に指定する必要があるのは前2つのテンプレートパラメータのみで、後2つはデフォルトのものが設定されています。このパラメータはそれぞれ

  • T : 要素型
  • E : エクステント(次元数とその要素数
  • L : レイアウトポリシー型
  • A : アクセサポリシー型

となっています。

// mdspnaの宣言例
namespace std {
  template<
    class T,
    class Extents,
    class LayoutPolicy = std::layout_right,
    class AccessorPolicy = std::default_accessor<T>
  >
  class mdspan;
}

例えばこんな感じで使用します

template<typename T>
using mat33 = std::mdspan<T, std::extents<std::size_t, 3, 3>>;

int main() {
  int storage[] = {
    0, 1, 2,
    3, 4, 5,
    6, 7, 8
  };

  // CTADによって要素型を推論
  mat33 A{storage};

  for (int y = 0; y < 3; ++y) {
    for (int x = 0; x < 3; ++x) {
      std::cout << A[y, x] << " ";
    }
    std::cout << '\n';
  }  
}
0 1 2 
3 4 5 
6 7 8 
  • Compiler Explorer (gcc13.1)
    • 参照しているヘッダの実装が間違っているためCTADが失敗するので、要素型を明示指定している

デフォルトのL, AによるアクセスはC++の配列へのアクセス同様の行優先レイアウトとなります。

レイアウトポリシーLを変更することによって考慮するレイアウトを変更することができ、例えば列優先レイアウトにする場合はLstd::layout_leftに変更します。

// レイアウトを行優先に
template<typename T>
using mat33 = std::mdspan<T, std::extents<std::size_t, 3, 3>, std::layout_left>;

int main() {
  int storage[] = {
    0, 1, 2,
    3, 4, 5,
    6, 7, 8
  };

  mat33 A{storage};

  for (int y = 0; y < 3; ++y) {
    for (int x = 0; x < 3; ++x) {
      std::cout << A[y, x] << " ";
    }
    std::cout << '\n';
  }  
}
0 3 6 
1 4 7 
2 5 8

std::mdspanは参照する領域のポインタとL, AELに保存される)をメンバとして持ち、簡単には次のような動作をしています

namespace std {
  template<
    class T,
    class Extents,
    class LayoutPolicy = std::layout_right,
    class AccessorPolicy = std::default_accessor<T>
  >
  class mdspan {
    // 領域ポインタ
    T* ptr;
    // インデックス計算
    LayoutPolicy::mapping<Extents> map;
    // アクセスを行う
    AccessorPolicy acc;
  
  public:

    ...

    template<class... OtherIndexTypes>
    auto operator[](OtherIndexTypes... indices) const {
      // 多次元インデックスから1次元のインデックスを求める
      auto idx = map(indices...);

      // 要素ポインタとインデックスから要素を引き当て
      return acc(ptr, idx);
    }
  };
}

動作の流れとしてはこんな感じですが、これは単純な例であり実際にはもう少し複雑になっています。

レイアウトポリシーLは多次元のインデックス列から1次元のインデックス空間への写像で、例えば幅Wの2次元領域なら、y, xの2次元インデックスに対してy * W + xを計算するものです。

アクセサポリシーAはインデックスiと領域ポインタptrからどのように要素を取得するかをカスタマイズする型で、基本的には*(ptr + i)を返します。ここではこれはカスタムしませんが、カスタマイズの方向性としては例えば、std::atomic_refを通してアクセスするとか、std::assume_alignedを通してアクセスするなどがあります。

このように、多次元配列アクセスにおいてやるべきことをレイアウトポリシーとアクセサポリシーで分割してハンドルし、かつそれをテンプレートパラメータで受け取ることによって柔軟にカスタマイズできるようにしています。

従って、インターリーブレイアウトをmdspanで扱うためには、レイアウトポリシー型Lをカスタマイズする必要があります。

レイアウトポリシー型の構造

レイアウトポリシー型をLPとすると、レイアウトマッピングクラス型はmappingという名前でLP::mappingのようにアクセスできる必要があり、唯一のテンプレートパラメータとしてExtentsstd::mspan<T, E, L, A>E)を受け取ります。

// レイアウトポリシー型
struct LP {

  // レイアウトマッピングクラス
  template <class Extents>
  class mapping {
    ...
  };
};

このExtentsstd::mdspanから供給されるもので、std::extentsの特殊化となります。レイアウトマッピングクラス内からは、このExtentsとそのオブジェクトを介して現在のstd::mdspanが参照している配列のサイズ(次元と次元ごとの要素数)を取得することができます。

レイアウトポリシー型はこのレイアウトマッピングクラスを定義しておくことだけが役割であり、実際のレイアウトカスタマイズはレイアウトマッピングクラス内部で行います。

今回は行優先でインターリーブされたレイアウトをハンドルするのでLPの名前はlayout_right_interleavedにすることにして、レイアウト計算のために必要となるパラメータとしてインターリーブされている配列数Dを受け取っておきます。

// レイアウトポリシー型
template<std::unsigned_integral auto D>
struct layout_right_interleaved {

  // レイアウトマッピングクラス
  template <class Extents>
  class mapping {
    ...
  };
};

このような構造になっているのは、mdspan<T, E, L>のようにテンプレートパラメータで渡した時に、LL<E>のようにEに依存してしまうのを避けるためだと思われます。これは記述が冗長となるほか、CTADが難しくなります。

レイアウトマッピングクラスの要件

レイアウトマッピングクラスには満たすべき性質や定義すべき関数などの要件がいくつもあります。

型に対する要件

レイアウトマッピングクラス型に関しては次のことが要求されます。

これを満たすためには通常、コピーコンストラクタとコピー代入演算子、および同値比較演算子default定義しておきます。

template<std::unsigned_integral auto D>
struct layout_right_interleaved {
  template <class Extents>
  class mapping {
  public:
  
    mapping(const mapping &) = default;
    mapping &operator=(const mapping &) & = default;

    friend bool operator==(mapping, mapping) = default;
  };
};

レイアウトマッピングクラス型は通常Extentsのオブジェクト1つを保持するだけで足りるため、コピーコストが低い型となるためこれで十分です。もし変なレイアウトのハンドル時にムーブが効率的となる場合は、ムーブコンストラクタ/代入演算子を定義したり、operator==の引数型を参照にしたりといったことをすればいいでしょう。

ここからは必須の要件ではありませんが、レイアウトマッピングクラスの他のコンストラクタの存在はstd::mdspanのコンストラクタの利用可能性に影響を与えます。

  • デフォルトコンストラク
    • std::mdspanのデフォルトコンストラクタを有効にするために必須
  • Extentsを受け取るコンストラク
    • 動的Extentsをサポートする場合に必要
    • デフォルトコンストラクタ以外の、std::mdspanのレイアウトマッピングクラスを受け取らないコンストラクタを有効にするために必須
  • 他のレイアウトマッピングクラスからの変換コンストラク
    • std::mdspanの変換コンストラクタを有効にするために必須

最後のレイアウトマッピング変換は必ずしもサポートできるとは限らないため任意ですが、残りの2つは可能ならば提供しておきましょう。std::mdspanの構築は複雑なので、これらのコンストラクタを欠いているとそのレイアウトマッピングを使用したstd::mdspanの構築が難しくなる可能性があります。

template<std::unsigned_integral auto D>
struct layout_right_interleaved {
  template <class Extents>
  class mapping {
    // エクステントを保存しておく(動的エクステントを使用する場合に必要)
    Extents m_extent;
  public:

    // デフォルトコンストラクタ
    mapping() = default;

    // エクステントを受け取るコンストラクタ
    constexpr mapping(const Extents& ex)
      : m_extent{ex}
    {}
  
    mapping(const mapping &) = default;
    mapping &operator=(const mapping &) & = default;

    friend bool operator==(mapping, mapping) = default;
  };
};

エクステントを受け取るコンストラクタにはconst Extents&が渡されるので、const参照か値で受ける必要があります。

これ以外のコンストラクタは必要なら定義することができ、これらのものと曖昧にならなければそれは自由です。

メンバ型

メンバ型は次の4つが要求されます

  • extents_type : テンプレートパラメータExtents
    • std::extentsの特殊化であること
  • index_type : 計算結果のインデックスの型
    • extents_type::index_type
  • rank_type : ランク(次元数)の型
    • extents_type::rank_type
  • layout_type : レイアウトマッピングクラスを包むレイアウトポリシー型
    • レイアウトマッピングクラスがLP::mappingのようになっている時のLP
    • std::mdspanのレイアウトマッピングクラスを受け取るコンストラクタがレイアウトポリシー型を取得するのに使用される

この4つはおそらく多くの場合ほとんどコピペで済むようなコードになります。

template<std::unsigned_integral auto D>
struct layout_right_interleaved {
  template <class Extents>
  class mapping {
  public:

    // メンバ型定義
    using extents_type = Extents;
    using index_type = typename extents_type::index_type;
    using rank_type = typename extents_type::rank_type;
    using layout_type = layout_right_interleaved<D>;    // 都度変更すべきはおそらくここだけ
  };
};

レイアウトの特性を表す関数

レイアウトがどういう性質を持つかについてを取得する関数が3種類要求されます。これらはすべて引数を取らずboolを返す関数です。

説明のために、レイアウトマッピングクラスのオブジェクトをmとしておきます

  • is_unique()
    • 配列の1つの要素に1つのインデックスだけが対応する場合にtrue
    • 対称行列における重複要素を節約するようなレイアウトだと1つの要素に複数のインデックスが対応するため満たさない
      • 2次元の場合m(i, j) == m(j, i)となるためユニークではない
  • is_exhaustive()
    • [0, m.required_span_size())の範囲内の全てのkに対して、m(idx...) == kとなる多次元インデックスidx...が存在する場合にtrue
      • 変換後のインデックスによるアクセスはメモリ上の要素全てにアクセスする(パディングがない)
      • 要素がメモリ上で連続していることとほぼ等しいが、変換後のインデックスが必ずしも連続的ではない場合があり、その場合でも与えられたメモリ範囲の要素全てに対応するインデックスが計算されることを表す
  • is_strided()
    • インデックス計算がストライドによって行われている場合にtrue
    • w高さ任意の2次元行列なら、多次元インデックスj, iに対してm(j, i)w * j + iを返す。この時、各次元のストライド(w, 1)となる。
      • これと同等の計算によってインデックス計算が行われている場合にtrueを返す
  • is_always_unique() (静的メンバ関数
    • is_unique()が常にtrueとなるならtrue
  • is_always_exhaustive() (静的メンバ関数
    • is_exhaustive()が常にtrueとなるならtrue
  • is_always_strided() (静的メンバ関数
    • is_strided()が常にtrueとなるならtrue

これらの関数は全てその性質を満たしていればtrueを返しそうでなければfalseを返します。

is_xxx()に対するis_always_xxx()is_xxx()の性質が個別のオブジェクトによらず常に満たされている場合にtrueを返します。falseを返す場合は、その性質がオブジェクトごとに(その構築時パラメータによって)満たされないことがありうることを表します。

今回の場合、ある要素は1組のインデックスによってしかアクセスされないためis_unique()はつねにtrueであり、考慮する配列数Dが2以上の場合は参照するメモリ領域の全ての要素にアクセスしないためis_exhaustive()falseとなります。そして、ストライド計算によってインデックスを計算可能なので、is_strided()trueです(詳しくは後述)。

template<std::unsigned_integral auto D>
struct layout_right_interleaved {
  template <class Extents>
  class mapping {
  public:

    ...

    static constexpr bool is_unique() noexcept {
      return true;
    }

    static constexpr bool is_exhaustive() noexcept {
      return D == 1;
    }

    static constexpr bool is_strided() noexcept {
      return true;
    }

    static constexpr bool is_always_unique() noexcept { 
      return true;
    }

    static constexpr bool is_always_exhaustive() noexcept { 
      return D == 1;
    }

    static constexpr bool is_always_strided() noexcept {
      return true;
    }
  };
};

staticメンバ関数は非静的メンバ関数と同様に.によるメンバアクセスで呼び出すことができるので、返す値がオブジェクトによらず決定するならばalwaysではない関数も静的メンバ関数として定義しておくことができます。

基本関数

レイアウトの計算およびそれに関する情報を取得する関数が4つ要求されます

  • extents()
    • 現在のエクステントを返す
  • operator(...)
    • 多次元インデックスをメモリ上の一点のインデックスに変換する
    • インデックス計算の実装はここで行う
  • required_span_size()
    • 現在のレイアウトがアクセスするメモリ範囲の最大値を返す
      • extents()のサイズが0なら0
      • それ以外の場合、インデックス計算の結果の最大値+1
  • stride(r)

extents()だけはそのままですが、operator()および他のものはレイアウトのインデックス計算の実装と関わってくるためインデックス計算実装時に整えます。

template<std::unsigned_integral auto D>
struct layout_right_interleaved {
  template <class Extents>
  class mapping {
    Extents m_extent;
  public:

    ...
  
    constexpr auto extents() const noexcept -> const extents_type& {
      return m_extent;
    }

    constexpr auto required_span_size() const -> index_type;

    constexpr auto stride(rank_type r) const -> index_type;

    template<typename... Indices>
      requires (sizeof...(Indices) == extents_type::rank()) and
               (std::is_nothrow_convertible_v<Indices, index_type> && ...)
    constexpr auto operator()(Indices... idx) const -> index_type;
  };
};

エクステント型の静的メンバ関数extents_type::rank()はそのエクステントのランク(次元数)を取得するもので、これは必ずコンパイル時の定数となります。静的constexprメンバ関数であるため、型の文脈でも使用することができます。

インターリーブレイアウトにおけるインデックス計算

レイアウトマッピングクラスの役割は、d次元の多次元インデックスi_0, i_1, ..., i_(d-1)を1次元の空間インデックスに変換することです。結果のインデックスがどのように使われるのかはmdspanのアクセサポリシークラスが決めることですが、通常のポインタ演算(計算結果のインデックスidxとストレージポインタptrに対して*(ptr + idx))を仮定して良いでしょう。したがって、レイアウトマッピングクラスでは要素のサイズとかバイト単位のアクセスとかを気にする必要はなく、要素サイズを1単位とした1次元領域上でのインデックス計算のみを考えれば良いわけです。

今実装したい配列(行列)のインターリーブレイアウトとは、複数の配列の対応するインデックスの要素を連続的に配置していくものでした。例えば3つの2次元行列を1つの2次元行列に行優先でインターリーブするとは次のようになります

// この3つの配列を
int A[] = {
  100, 101, 102,
  110, 111, 112,
  120, 121, 122,
};
int B[] = {
  200, 201, 202,
  210, 211, 212,
  220, 221, 222,
};
int C[] = {
  300, 301, 302,
  310, 311, 312,
  320, 321, 322,
};

// このように詰める
int interleaved[] = {
  100, 200, 300, 101, 201, 301, 102, 202, 302,
  110, 210, 310, 111, 211, 311, 112, 212, 312,
  120, 220, 320, 121, 221, 321, 122, 222, 322,
};

2次元の場合により一般化して考えてみると

1つのインターリーブ配列の中に含まれる2次元行列の数をD、行列の行数をJ、行列の列数をIとして、d = D - 1i = I - 1j = J - 1とすると

 \displaystyle
M_{int} = \begin{pmatrix}
a_{000} & ... & a_{d00} & a_{001} & ... & a_{d01} & ... & a_{00i} & ... & a_{d0i} \\
a_{010} & ... & a_{d10} & a_{011} & ... & a_{d11} & ... & a_{01i} & ... & a_{d1i} \\
\vdots & \vdots & \vdots & \vdots & \vdots & \vdots & \vdots & \vdots & \vdots & \vdots \\
a_{0j0} & ... & a_{dj0} & a_{0j1} & ... & a_{dj1} & ... & a_{0ji} & ... & a_{dji}
\end{pmatrix}

こんな感じになります。

1次元方向(行方向)を見てみると、あるn番目の行列の隣り合う要素(例えばa_{n00}a_{n01})の間には含まれる行列分、つまりD個の要素があります。そのため、ある1つの行列の行内でl番目の要素のインデックスはl * Dで求められます。

2次元方向(列方向)を見てみると、あるn番目の行列の上下で隣り合う要素(例えば a_{n00}  a_{n10} )の間には、含まれる行列全ての列要素の個数分の要素、つまりD * I(行列数 × 列幅)個の要素があります。そのため、ある1つの行列の列間のオフセットはm * D * Iで求められます。

したがって、ある1つの行列の先頭要素から見た時、その行列のyx行の要素へのインデックスidx

 \displaystyle
idx = y \times D \times I + x \times D

で求められます。

多次元行列への一般化

含まれる行列を3次元以上にしたくなる場合があるかもしれません。3次元以上となるとイメージを描くのも難しくなりますが、3次元行列の配置がどうなるのかについては2次元行列の要素を1次元配列と思うことで考えることができます。あるいは、何次元だろうが1次元配列に詰めてしまうことを考えると、3次元配列とは2次元配列の配列であり、N次元配列とはN-1次元配列の配列です。

つまり、普通の3次元行列の3次元軸方向に隣り合う要素(例えば a_{000}  a_{100} )の間には、その行列の一部である2次元行列1つ分の要素が詰まっています。

その上でインターリーブされている場合のインデックスを考えると、インターリーブされているある3次元行列の3次元軸方向に隣り合う要素(例えば a_{n000}  a_{n100} )の間には、インターリーブされている行列の個数分の2次元行列が間に挟まっています。

インターリーブされている行列数をD、3次元行列の各次元の要素数を1次元目からI, J, Kとすると、ある1つの行列の先頭要素から見た時、その行列の(x, y, z)(左側が低位次元)要素へのインターリーブされた空間上でのインデックスidx

 \displaystyle
idx = z \times D \times I \times J + y \times D \times I + x \times D

で求められます。

同様に一般化すると、インターリーブされている行列数をD、N次元行列の各次元のサイズ(要素数)をIn0 <= n < N)とすると、ある1つの行列の先頭要素から見た時、その行列の(i0, ..., in)要素へのインターリーブされた空間上でのインデックスidxは、

 \displaystyle
idx = D \times (i_n \times (I_{n - 1} \times ... \times I_0) + ... + i_2 \times I_1 \times I_0 + i_1 \times I_0  + i_0)

のようになります(多分)。

今回は2次元行列のインターリーブだけを確認することにして、高次元はとりあえずこれに則って実装はしますが特にチェックしないことにします(間違ってたら教えてください)。

実装

後は上式をコードに直すだけです。色々な方法が考えられますが、ここでは上式をホーナー法によって変換して、それをそれを実装する事にします。

 \displaystyle
idx = D \times (I_0 \times ( I_1 \times ...(I_{n - 2} \times (I_{n - 1} \times i_n + i_{n-1}) + i_{n-2})... + i_1) + i_0)

ここでのインターリーブされている行列数Dはレイアウトポリシー型の非型テンプレートパラメータDから、行列の次元数Nはエクステント型のextents_type::rank()から、各次元の要素数I_iはエクステントオブジェクトのメンバ関数m_extent.extent(i)から、多次元インデックスi_nはレイアウトマッピング型のoperator()の引数から取得できます。

template<std::unsigned_integral auto D>
struct layout_right_interleaved {
  template <class Extents>
  class mapping {
    Extents m_extent;
  public:

    ...

    constexpr auto required_span_size() const -> index_type {
      // 丸投げ
      return layout_stride::mapping<Extents>(m_extent).required_span_size();
    }

    // 次元rのインデックスにかけられている係数を求める
    constexpr auto stride(rank_type r) const -> index_type
      requires (extents_type::rank() != 0)
    {
      assert(r < extents_type::rank());

      index_type stride = D;

      for (auto i : std::views::iota(0u, r)) {
        stride *= m_extent.extent(i);
      }

      return stride;
    }

    template<typename... Indices>
      requires (sizeof...(Indices) == extents_type::rank()) and
               (std::is_nothrow_convertible_v<Indices, index_type> && ...)
    constexpr auto operator()(Indices... indices) const -> index_type {
      // 行列次元数
      // extent()が0indexなので最大値は-1する
      constexpr std::unsigned_integral auto N = extents_type::rank() - 1;
      static_assert(0u < N);

      // インデックス配列
      // indicesは先頭が最大次元、末尾が1次元
      const std::array<index_type, extents_type::rank()> idx_array = {static_cast<index_type>(indices)...};

      index_type idx = idx_array[0];

      for (auto m = N - 1; const auto in : idx_array | std::views::drop(1)) {
        idx *= m_extent.extent(m);
        idx += in;
        --m;
      }

      return D * idx;
    }
  }
};

計算してる部分(operator())では、先ほどのホーナー法による式の内側から計算しています。このレイアウトマッピングクラスのoperator()の引数の多次元インデックスは、mdspanoperator[]に渡されるものがそのまま渡され、先頭が最大次元で末尾が1次元のインデックスとなるような順番で渡ってきます。

std::arrayに格納しているのはパラメータパックのままだと取り扱いが面倒だったためで、パック展開を駆使すればもっといい感じにできる可能性があります。

範囲for内では、次元nのインデックスinm = n - 1次元のサイズImをかけてから、n + 1次元のインデックスを足し合わせ、mの次元を1つ落としてループします。この時、mが先に-1に到達しモジュロ演算で最大値になってしまうのでそのmにはアクセスしないようにする必要があり、先頭インデックス(idx_array[0])を先に取ってそれを飛ばした残りの要素でループを回しているのはその対策のためです。

required_span_size()の実装を委譲しているのは、多次元インデックス空間が0の時とランクが0の時をハンドルするのとか最大範囲を求めるのが面倒だとかの理由によるものです。stride(r)required_span_size()の実装の意味については後述します。

これをこんなコードで簡単にテストすると

int main() {
  using test = layout_right_interleaved<3u>::mapping<extents<std::size_t, 3, 3>>;

  test m{extents<std::size_t, 3, 3>{}};

  std::cout << m.stride(0) << '\n';
  std::cout << m.stride(1) << '\n';

  std::cout << "(0, 0) -> " << m(0, 0) << '\n';
  std::cout << "(0, 1) -> " << m(0, 1) << '\n';
  std::cout << "(1, 0) -> " << m(1, 0) << '\n';
  std::cout << "(1, 1) -> " << m(1, 1) << '\n';
  std::cout << "(2, 2) -> " << m(2, 2) << '\n';
}

こんな出力が得られます

3
9
(0, 0) -> 0
(0, 1) -> 3
(1, 0) -> 9
(1, 1) -> 12
(2, 2) -> 24

多分合ってそうです。

これをmdspanに組み込みます。mdspanのテンプレートパラメータはmdspan<T, E, L, A>の順で、今回Aは弄らずLを変えたいためそこまでの3つのテンプレートパラメータの手動指定が必要です。

mdspanでは少なくとも要素型とエクステント型は都度指定する必要があるので、mdspanを利用する際はあらかじめ必要なテンプレートパラメータを埋めた型エイリアスを作成しておくと便利です。

// 3x3行列D個のインターリーブ配列を参照するmdspan
template <typename T, std::unsigned_integral auto D>
using interleaved_mat33 = mdspan<T, extents<std::size_t, 3, 3>, layout_right_interleaved<D>>;

これを、次のようなコードでテストすると

// 3x3 mdspanを受け取り出力
template <typename T, typename L>
void print_mat(mdspan<T, extents<std::size_t, 3, 3>, L> mat33) {
  for (int y = 0; y < 3; ++y) {
    for (int x = 0; x < 3; ++x) {
      std::cout << mat33[y, x] << ' ';
    }
    std::cout << '\n';
  }
  std::cout << '\n';
}

int main() {
  // 3x3行列を3つインターリーブ
  int storage[] = {
    111, 211, 311, 112, 212, 312, 113, 213, 313,
    121, 221, 321, 122, 222, 322, 123, 223, 323,
    131, 231, 331, 132, 232, 332, 133, 233, 333
  };

  // それぞれの行列の先頭要素のポインタを渡す
  interleaved_mat33<int, 3u> A{storage};
  interleaved_mat33<int, 3u> B{storage + 1};
  interleaved_mat33<int, 3u> C{storage + 2};

  print_mat(A);
  print_mat(B);
  print_mat(C);
}

次のような出力が得られます

111 112 113 
121 122 123 
131 132 133 

211 212 213 
221 222 223 
231 232 233 

311 312 313 
321 322 323 
331 332 333 

どうも正しくインターリーブされた行列を参照できているようです。

今回のレイアウトポリシー型では先頭要素からの相対インデックスを計算するようにしたのでmdspanの参照する領域は参照したい行列の先頭要素から始まる必要があり、初期化時には少なくとも先頭要素のアドレスを計算する必要があります。

ストライドlayout_stride

先ほど求めたインターリーブレイアウトのインデックス計算の各次元でインデックスに対して固定的に積算されている値、例えばidx = y * D * I + x * Dyに対するD * Ixに対するDは各次元における要素のずらし幅となっています。例えば、D = 1(つまりインターリーブなし)とすると、通常の2次元インデックス計算の式に一致することがわかるでしょう。

このずらし幅は一般化されストライドと呼ばれ、多次元配列に対するストライドはこのインターリーブを含めてさまざまなレイアウトを表現することができます。

レイアウトマッピングクラス型のメンバ関数として要求されていたis_strided()is_allways_strided()trueを返すとは、このストライドによってレイアウト計算されていることを表します。そして、レイアウトマッピングクラスに要求されるstride(r)というのは、r次元におけるストライドr次元のインデックスにかけられる係数)を求めるための関数です。それはm次元のサイズをI(m)と表すと、n次元のインデックスinに対してD * I(n-1) * ... * I(0)のように計算されます。

ストライドによってさまざまなレイアウトの配列を表現できることはよく知られているため、mdspanにはデフォルトで任意のストライドを扱うことのできるレイアウトポリシー型であるstd::layout_strideが用意されています。インターリーブレイアウトをハンドルするためにはこんな手間をかけてレイアウトポリシー型を自作しなくてもこれを使用すると簡単に処理できます。

std::layout_strideは各次元に対するストライドを手動で指定することで、任意のストライドによるレイアウトを表現することができます。ストライド値は、レイアウトマッピング型(std::layout_stride::mapping)のコンストラクタにstd::arrayで渡します。

// 要素型Tの3x3インターリーブ行列
template <typename T>
using stride_interleaved_mat33 = mdspan<T, extents<std::size_t, 3, 3>, layout_stride>;

int main() {
  // 3x3行列を3つインターリーブ
  int storage[] = {
    111, 211, 311, 112, 212, 312, 113, 213, 313,
    121, 221, 321, 122, 222, 322, 123, 223, 323,
    131, 231, 331, 132, 232, 332, 133, 233, 333
  };

  // レイアウトマッピング型取り出し
  using mapping = stride_interleaved_mat33<int>::mapping_type;

  // この場合の各次元のストライド(右側ほど低次元)
  // 要素型はextentsの要素型に変換できればなんでもいい
  constexpr std::array<std::size_t, 2> stride = {9, 3};

  // CTADによりテンプレートパラメータを推論している
  stride_interleaved_mat33 A{storage,     mapping{{}, stride}};
  stride_interleaved_mat33 B{storage + 1, mapping{{}, stride}};
  stride_interleaved_mat33 C{storage + 2, mapping{{}, stride}};

  print_mat(A);
  print_mat(B);
  print_mat(C);
}

このlayout_strideに対する手間をかけて作成したlayout_right_interleavedのメリットは

などでしょうか。このメリットにこの手間が釣り合うと考える場合は自作する価値があるかもしれません(今回はお勉強のためなのでメリットとか無視してます)。

大抵のメモリレイアウトはストライドを工夫することで表現できるので、mdspanでカスタムレイアウトを取り扱おうと思い立った時はまずlayout_strideの利用を検討してみるといいかもしれません。更なる最適化が欲しかったり、ストライドでは表現できない場合にレイアウトポリシー型の自作に進むと良いでしょう。

インターリーブレイアウトはlayout_strideによって表現可能なので、layout_right_interleavedの特性はlayout_strideのそれと同じになります。そのため、required_span_size()のような少し面倒な実装はlayout_strideの実装を流用することができます。

静的エクステントの場合の最適化

ひとまず完成したlayout_right_interleavedの実装は、extents_typestd::extents)の各次元の要素数が動的な場合でも対応可能な実装になっています。動作例がそうであるように、全ての次元の要素数コンパイル時に既知であれば、計算の一部をコンパイル時に終わらせておくことができます。

エクステントが完全に静的であるか(動的エクステントが含まれていないかどうか)は、std::extentsの静的メンバ関数であるrank_dynamic()0を返すかどうかで判定できます。これは静的constexpr関数なのでコンパイル時に呼び出すことができ、コンセプトなどの制約にも用いることができます。

まず、エクステントが静的である場合に、各次元のストライドコンパイル時に求めておきます。

template<std::unsigned_integral auto D>
struct layout_right_interleaved {
  template <class Extents>
  class mapping {
    [[no_unique_address]]
    Extents m_extent;
  public:
    using extents_type = Extents;
    using index_type = typename extents_type::index_type;
    using rank_type = typename extents_type::rank_type;
    using layout_type = layout_right_interleaved<D>;
  
  private:

    // コンパイル時のストライド計算
    static constexpr auto calc_static_stride() -> std::array<index_type, Extents::rank()> {
      // 動的エクステントの場合は空
      if constexpr (Extents::rank_dynamic() != 0) {
        return {};
      }

      // 全て静的なら、各次元のストライドを求める
      std::array<index_type, Extents::rank()> stride;

      stride[0] = D;

      for (auto i = 1u; i < Extents::rank(); ++i) {
        stride[i] = stride[i - 1] * Extents::static_extent(i - 1);
      }

      return stride;
    }

    // コンパイル時に求めたストライド
    static constexpr std::array<index_type, Extents::rank()> static_stride = calc_static_stride();

    ...
  };
};

静的メンバ変数として保存しておくことでコンパイル時に使用でき、オブジェクトのサイズを消費しないようにします。

次に、これを用いてインデックス計算周りを書き換えます。

template<std::unsigned_integral auto D>
struct layout_right_interleaved {
  template <class Extents>
  class mapping {
    ...

    // コンパイル時に求めたストライド
    static constexpr std::array<index_type, Extents::rank()> static_stride = calc_static_stride();
  public:

    constexpr auto stride(rank_type r) const -> index_type
      requires (extents_type::rank() != 0)
    {
      assert(r < extents_type::rank());

      if constexpr (Extents::rank_dynamic() != 0) {
        index_type stride = D;

        for (auto i : std::views::iota(0u, r)) {
          stride *= m_extent.extent(i);
        }

        return stride;
      } else {
        // 全て静的ならあらかじめ計算したものを返す
        return static_stride[r];
      }
    }

    template<typename... Indices>
      requires (sizeof...(Indices) == extents_type::rank()) and
               (std::is_nothrow_convertible_v<Indices, index_type> && ...)
    constexpr auto operator()(Indices... indices) const -> index_type {
      // 行列次元数
      // extent()が0indexなので最大値は-1
      constexpr std::unsigned_integral auto N = extents_type::rank() - 1;
      static_assert(0u < N);

      // インデックス配列
      // indicesは先頭が最大次元、末尾が1次元
      const std::array<index_type, extents_type::rank()> idx_array = {static_cast<index_type>(indices)...};

      if constexpr (Extents::rank_dynamic() != 0) {
        // 動的エクステントを含む場合の計算

        index_type idx = idx_array[0];

        for (auto m = N - 1; const auto in : idx_array | std::views::drop(1)) {
          idx *= m_extent.extent(m);
          idx += in;
          --m;
        }

        return D * idx;
      } else {
        // 全て静的エクステントな場合の計算

        index_type idx = 0;

        // 求めたストライドは先頭が1次元になっているので、反転させる
        for (index_type i = 0; const auto st : static_stride | std::views::reverse) {
          idx += st * idx_array[i];
          ++i;
        }

        return idx;
      }
    }

  };
};

Compiler Explorerアセンブリ出力対応の色付けを見ると、静的と動的の両方の処理がきちんと使われており、出力も正しいことがわかります。

あらかじめストライドが求めてある場合、各次元のインデックスに対応するストライドをかけて足すだけなので処理はだいぶ簡単になります。ここではやっていませんが、それによって畳み込み式で簡単に書けるようになると思われます。ただし、コンパイル時にストライドを求めるようにする場合でも、実行時の処理と掛け算と足し算の回数がほぼ変わらないのであまり効率的にはならないかもしれません。

また、std::extentsはエクステントが全て静的である場合にサイズが1になるので、これによってエクステントが静的ならlayout_right_interleavedは空のクラスとなりそのサイズも1になります(EBOが働く場合)。これはmdspanをコピーするときのコストを低下させることにつながります。

ソースコード全体

参考文献

この記事のMarkdownソース

[C++] 式のstatic type

式の静的型(static type)は参照型にならない、すなわち、式の静的型は値カテゴリの情報を含まない、あるいは、式の静的型と式の値カテゴリは直行する概念である、ということに関するメモです。

以下、規格書の文面はC++20規格と同等のドラフトであるN4861を参照します。

なお、この記事はC++の規格書を読む場合にたまーに問題になることがある概念についてのものであって、通常のC++プログラミングで気にする必要は全くありません。普通式の型と言ったら、値カテゴリの情報も含んだ型です(つまり参照型になりうる)。

static typevalue category

static typeとは[defns.static.type]で次のように定義されています

type of an expression ([basic.types]) resulting from analysis of the program without considering execution semantics

実行時意味論を考慮しないでプログラムを解析した結果得られる、式の型

「実行時意味論を考慮しないでプログラムを解析した結果得られる」とはおおよそ、C++の規則に従ってコンパイルしていく過程で得られる、という意味です。static typeとはその字面の通り、コンパイル時に定まっている型のことだと思って構いません。対になる概念としてdynamic typeというものがあり、こちらは式の実行時に定まる式の型として定義されています。

式の型とは[expr.type]で定義されています。特に、その最初の項には次のようにあります

If an expression initially has the type “reference to T” ([dcl.ref], [dcl.init.ref]), the type is adjusted to T prior to any further analysis. The expression designates the object or function denoted by the reference, and the expression is an lvalue or an xvalue, depending on the expression.

式の最初の型がTへの参照(T&/T&&)である場合、以降の解析の前に式の型はTに調整される。 式は参照によって示されるオブジェクトまたは関数を指定し、式によってlvalueまたはxvalueとなる。

ついでに、次の項にはprvalueの型に関して次のようにあります

If a prvalue initially has the type “cv T”, where T is a cv-unqualified non-class, non-array type, the type of the expression is adjusted to T prior to any further analysis.

prvalueの最初の型がcv Tとなり、TがCV修飾されていない非クラス型、非配列型である場合、以降の解析の前に式の型はTに調整される

ここで、式(prvalue)の最初の型とは、式の種類ごとに個別に式の型として指定されているものです。また、式の値カテゴリの決定に関しても同様に式の種類ごとに指定されています。

式の型 ⊥ 式の値カテゴリ

静的型(static type)はコンパイル時にわかる式の型のことを言い、式の型は、式の種類によって決まる型から参照を取り除いた型になります。従って、静的型は参照型になることはありません。

cppreferenceの値カテゴリのページにも次のようにあり

Each C++ expression (an operator with its operands, a literal, a variable name, etc.) is characterized by two independent properties: a type and a value category. Each expression has some non-reference type, and each expression belongs to exactly one of the three primary value categories: prvalue, xvalue, and lvalue.

C++の各式は型と値カテゴリという二つの独立したプロパティによって特徴付けられる。各式は何らかの非参照型を持ち、3つの主要な値カテゴリ(prvalue, xvalue, lvalue)のどれか1つに属している。

この記述からも、静的型は参照型にならず、静的型と値カテゴリは式の持つ直交した性質であることは間違いないようです。

影響

規格書中では静的型(static type)という言葉は時々出てきますが、それはほとんどの場合式(あるいはオブジェクト)のコンパイル時に定まる型というふわっとした理解でも困ることはありません。

N4861中のstatic typeの出現箇所(goole検索による)

おそらく唯一その違いを認識する必要があるところは、例外オブジェクトの型を決めるところです。例外オブジェクトの型はそれを送出したthrow式のオペランド(式)の静的型から決定されますが、ここで静的型を参照していることによって例外オブジェクトの型は決して参照型になりません。以降の例外ハンドラでのマッチングなどはこれを前提にされていて、参照型を考慮していません。そのため、ここだけは式の静的型が参照型ではないことを認識しておく必要があります。他のところでは、おそらく気にしなくても行けてしまう気がします。

参考文献

この記事のMarkdownソース

[C++] 例外送出からキャッチまでのあいだ

C++throw式はどんな型のオブジェクトであっても投げることができます。この是非は置いておいて、あるthrow式に対して適切にcatch節(例外ハンドラ)が用意されている場合に、呼び出される例外ハンドラは厳密にどのように決まるのでしょうか?なんとなくthrow式の引数と同じような型ならマッチする気はしますが、そのルールは関数のオーバーロード解決時のものとは異なる気がします。

catch節での型マッチング

例外オブジェクトの型をEとすると、対応する例外ハンドラの決定はそのcatch節の宣言型(以下、ハンドラの型)とEをマッチングすることによって行われます。マッチングとはオーバーロード解決のような複雑な処理とは異なり、単純な型の比較によって行われます。

マッチングの前にまず、ハンドラの型が配列型(T[])もしくは関数型(R(Args...))である場合そのハンドラの型はdecayされ、Tの配列型はTのポインタ型(T[] -> T*)、関数型は関数ポインタ型(R(Args...) -> R(*)(Args...))としてマッチングされます。

その上で、例外オブジェクトの型Eとハンドラの型は次のいずれかの場合にマッチしているとみなされます

  • ハンドラの型がcv Tもしくはcv T&で、ETが(トップレベルのCV修飾を無視して)同じ型の場合
  • ハンドラの型がcv Tもしくはcv T&で、TEの曖昧でないpublicな基底クラスである場合
    • すなわち、std::derived_from<E, T>trueとなる場合
  • ハンドラの型がcv Tもしくはconst T&で、T, Eが共にポインタ/メンバポインタ型かつ、Eが次のどれか(1つ以上)の変換によってTに変換可能である場合
    • private/protectedもしくは曖昧な(基底)クラスへの変換を含まない、標準ポインタ変換
    • 関数ポインタ変換
    • 修飾変換
  • ハンドラの型がcv Tもしくはconst T&で、Tが共にポインタ/メンバポインタ型かつ、Estd::nullptr_tの場合
  • ハンドラの型が...で宣言されている場合

ここでのcvとは省略可能なconst/volatile修飾を表しています。とはいえポインタや参照型の参照先に対するもの(非トップレベルのCV修飾)以外にvolatileが意味を持つとは思えないので、ここではconstだと考えればいいでしょう。

トップレベルのCV修飾はポインタ型の場合T const * const(=const T * const)のような宣言における*の右側にくるCV修飾がトップレベルのCV修飾となります。したがって、1つ目の条件では例えば、int*int* constが同じ型とみなされ、int*const int*は同じ型だとはみなされないことになります。参照型ではトップレベルのCV修飾を行えないためint&const int&は異なる型とみなされ(ただし後述する理由によりこのマッチングは起こらない)、その他の型の場合はCV修飾があればそれがトップレベルのものなので、intconst intは同じ型とみなされます。これらのマッチングは、ハンドラの型で例外オブジェクトの型を受けることを考えると納得できると思います。

3つ目の条件における各種ポインタ変換は前提条件を満たした上で、おおよそE -> Tの暗黙変換が通る場合と言い換えることができます(基底クラスへの変換は制限がありますが)。ここではそのようにお茶を濁して複雑な暗黙変換の世界へは深入りしません、cppreferenceの暗黙変換ページをご参照ください・・・

そして、このようなマッチングは、実行時にtryブロック中で例外が投げられた場合にそれに付属するcatch節に対して実行時に行われ、例外ハンドラに対するマッチングの順序はソースコード上で現れている順番通りに行われます。このため、例外ハンドラの順番によっては必ずしも最適なマッチングとならない場合や決して呼ばれない例外ハンドラが存在することになりますが、例えコンパイル時にそれがわかっていたとしても並べ替えられたりはしません(コンパイラが優しいと警告はしてくれるかもしれません)。

#include <stdexcept>
#include <exception>

void f() noexcept(false);

int main() {
  try {
    f();
  }
  catch (int) {}  // #1
  catch (const char*) {}  // #2
  catch (const std::exception&) {}    // #3
  catch (const std::logic_error&) {}  // #4
}

この例の場合、f()から例外が投げられると対応する例外ハンドラのマッチングは#1 -> #2 -> #3 -> #4の順で行われます。そして、#4のハンドラは決して呼ばれることがありません。

この関数f()が別の翻訳単位(特にビルド済のdll/so)で定義されている場合、ここから投げられる例外は予測不可能となります。したがって、例外ハンドラのマッチングは実行時にしか行うことができず、実行時に(しかも例外発生時というクリティカルな状況で)それを行うためにオーバーロード解決のような複雑な手順をとることも憚られるため、上記のような比較的簡易なマッチングによって例外ハンドラを決定しているのだと思われます。

例外ハンドラで使えない型

例外ハンドラの型として次のものを指定するとコンパイルエラーとなります

  • 不完全型
  • 抽象クラス型
  • 右辺値参照型
  • 不完全型へのポインタ/参照
    • cv void*を除く
void f() noexcept(false);

// 不完全型
struct S;

// 抽象クラス型
struct A {
  virtual void f() = 0;
  virtual ~A() = default;
};

int main() {
  try {
    f();
  }
  catch (S) {}        // ng
  catch (A) {}        // ng
  catch (int&&) {}    // ng
  catch (const S*) {} // ng
  catch (const S&) {} // ng
  catch (A*) {} // ok
  catch (A&) {} // ok
}

抽象クラスは値として指定できないだけで、ポインタ/参照であれば指定可能です。

例外オブジェクトの型

ハンドラの型のマッチング方法は分かりましたが、それを考えようとするともう一つよく分からないことが出てきます。それは、例外オブジェクトの型Eがどう決まるのかです。それは当然throw式のオペランドの型から決まりますが、オペランドの型そのものではなく少し減衰されます。

例外オブジェクトの型Eは、throw式のオペランドの型(コンパイル時の静的型)をTとします。まず、このTは式の静的型であり値カテゴリの情報を含まないため参照型ではありません。そして、このTは次のような変換を受けます

  • TのトップレベルのCV修飾を除去する
  • Tが型Uの配列型もしくは関数型Uである場合、TUのポインタ型へdecayする

この時、Eが次のいずれかの型に該当する場合、コンパイルエラーとなります

  • 不完全型
  • 抽象クラス型
  • 不完全型へのポインタ
    • cv void*を除く

このようにして決定された型Eが例外オブジェクトの型となり、例外ハンドラのマッチングにおいてはこのEがそのまま使用されます。

変換をまとめると次のようになります

throw式のオペランドの型 例外オブジェクトの型
T T
const T T
T* T*
T* const T*
T const * T const *
T const * const T const *
R(Args...) R(*)(Args...)
T[] T*

繰り返しますが、throw式のオペランドの型を取得する時に参照(正確には値カテゴリ)は考慮されないため、throw式に参照を渡しても参照をスローすることはできません。それでもあえてthrow式のオペランド型で参照を考慮するとすると、例外オブジェクトの型は次のようになります

throw式のオペランドの型 例外オブジェクトの型
T& T
T&& T
const T& T

例外オブジェクトの状態

例外オブジェクトの型と例外ハンドラのマッチングの雰囲気をつかむと、throwprvalueを投げた時でもTに対してT&const T&で受けられる事に気づきます。C++の値カテゴリのルールから、const T&で受けられるのはさほど不思議ではないかもしれませんが、T&で受けられるのは少し奇妙です。

すると不思議になってくるのは、例外オブジェクトの状態、特に値カテゴリについてです。例外オブジェクトとはどこにいてどのような状態にあるのでしょうか?また、例外ハンドラでそれをキャッチしたとき、キャッチしているものは一体何なのでしょうか?

例外が投げられると、スタックの巻き戻しが始まり、例外オブジェクトをキャッチ可能な例外ハンドラに到達(この決定は先程の型マッチングによる)するまでスタックが巻き戻され、その間に存在したオブジェクトを破棄していきます。例外オブジェクトはこのスタック巻き戻しの間生存している必要があり、なおかつ例外が投げられたコンテキストにおけるスタックの深さは実行時に決まり、どこで例外がハンドルされるかも分かりません。また、どこかのハンドラでキャッチされた時でも、再スローされる可能性があるため、ハンドラとは無関係に生存している必要があります。

規格では、例外オブジェクトを配置するメモリ領域がどう確保されるかは未規定とされており、Itanium C++ ABI(多くの非Windows環境)ではヒープ領域、Windows(MSVC) ABIではスタック領域があてがわれるようです。

例外オブジェクトは、そのように確保された領域にthrow式のオペランドからコピー初期化されます。従って、throw式で一時オブジェクトやローカルオブジェクト(への参照)を投げた時でも、一旦通常のスタックとは隔離された領域にコピーされています。

例外オブジェクト初期化時のコピー初期化は、例外オブジェクトをex、その型をEthrow式のオペランド(式)をopとすると、次のような初期化と同じです。

E exobj = op;

前述のように、ここでのEは必ず非参照型になります。

従って、スタック巻き戻しが起きている間例外オブジェクトはそれとは無関係な領域でグローバルオブジェクトであるかのように生存しています。ただし、例外オブジェクトは一時オブジェクトの一種とされます。

例外ハンドラ引数の初期化

例外オブジェクトは型マッチングでマッチした最も近くにあるハンドラによって捕捉されます。捕捉するハンドラが確定した後、ハンドラの型が...ではなくそのハンドラの引数(catch(T arg)arg)が存在する場合、それは例外オブジェクトから初期化されます。

例外オブジェクトの型をE、ハンドラの型をcv Tもしくはcv T&、ハンドラの引数をargとすると、次のどちらかの経路で例外ハンドラの引数は初期化されます

  • TEの基底クラスの場合、argは例外オブジェクトのTに対応する基底クラスのサブオブジェクトを指定する左辺値からコピー初期化される
  • それ以外の場合、argは例外オブジェクトの左辺値からコピー初期化される

いずれにしてもコピー初期化されますが、その際に例外オブジェクトはいつも左辺値として扱われます。これは他の一時オブジェクトとは異なる扱いとなります。

コピー初期化と言いますが、ハンドラの型がcv T&である場合は例外オブジェクトへの参照が初期化されコピーは起こりません。

また、ハンドラの型の制約によって、Eがポインタ型以外の場合はここでは基底クラスへのスライシング以外の暗黙変換は起こりません。Eがポインタ型の場合は基底クラスのポインタへの変換も含めた暗黙変換が起こり得ます。

ここまでを理解すると、例外ハンドラの型として右辺値参照型が禁止されている理由も見えてきます。それは単純に、例外ハンドラが右辺値参照をキャッチすることはあり得ないからです。

コピー省略が起こるところ

Tのオブジェクトのコピー初期化とは、初期化するオブジェクトをobj、初期化式をinitとすると、次のような初期化と同じです

T obj = init;

ここでは、Tが非参照型(特にクラス型)であり、式initの値カテゴリがprvalueである場合にコピー省略がなされます。

例外のスローから例外ハンドラによるキャッチまでの経路では、throw式における例外オブジェクトの構築時にコピー省略が起こる可能性があります。例外オブジェクトの型Eは非参照型なのでthrow式のオペランドprvalueである場合に、例外オブジェクトはコピー省略によってthrow式のオペランドから直接初期化されます。

一方、例外ハンドラの引数を例外オブジェクトから初期化する際には、例外オブジェクトを左辺値としてコピー初期化しようとするため、コピー省略は起こりません。

struct Test {
  Test() {
    std::cout << "call default constructor.\n";
  }

  Test(const Test&) {
    std::cout << "call copy constructor.\n";
  }
};

int main() {
  try {
    throw Test{}; // デフォルトコンストラクタ呼び出しのみ
  }
  catch (Test t)  // コピーコンストラクタ呼び出し
  {
    std::cout << "exception handled.\n";
  }
}

このthrow Test{};Testのローカルオブジェクトを渡すように書き換えると、コピー省略が行われなくなります(throw式のオペランドが左辺値になるため)。

例外オブジェクトの寿命

例外オブジェクトはスタック巻き戻しが起きている間生存しています。どこかの例外ハンドラで例外オブジェクトがキャッチされたとしても、そこから再スローされると同じ例外オブジェクトがスローされます(つまり、再スロー時には例外オブジェクトは破棄されません)。

そんな例外オブジェクトが寿命の終わりを迎えるのは、次のどちらかの場所です

  1. 例外オブジェクトをキャッチした例外ハンドラが再スローせずに終了した場合
    • その例外ハンドラの引数が破棄された直後に、例外オブジェクトは破棄される
  2. 例外オブジェクトを参照するstd::exception_ptrオブジェクトが破棄された時
    • そのデストラクタがリターンする前に、例外オブジェクトは破棄される

ただし、どちらの場合でも必ずしも例外オブジェクトが破棄されるとは限りません。それは例えば、ある例外オブジェクトを参照するstd::exception_ptrが他の場所で生存している場合が該当します。

例外オブジェクトがこのような寿命を持つことから、例外ハンドラで非const参照で例外オブジェクトを受けてそのハンドラから再スローする場合に、例外オブジェクトに変更を加えることができます。

int main() {
  try {
    try {
      throw 20; // 例外オブジェクトはint型、20で初期化
    } catch (int& n) {
      std::cout << "catch ref : " << n << '\n'; // 20が出力される
      n = 30; // nは例外オブジェクトを直接参照している

      throw;  // 再スロー
    }
  } catch (int& n) {
    std::cout << "catch ref : " << n << '\n'; // 30が出力される
  }
}

std::exception_ptr

例外ハンドラの内部では、std::current_exception()によって現在の例外オブジェクトを指すstd::exception_ptrを取得することができます。std::exception_ptrで例外オブジェクトを捕捉しておくと、例外オブジェクトの寿命をその例外ハンドラ(スローから始まるスタック巻き戻しの間)の期間を超えて延長させることができ、また別のスレッドに運び出すことができます。

std::exception_ptrは例外オブジェクトの型を隠蔽したstd::shared_ptrとおおよそ同じような雰囲気で使用することができます。例えば、std::exception_ptrをコピーすると、同じ例外オブジェクトを参照するstd::exception_ptrが増え、どちらかが破棄されてももう片方が生存していれば例外オブジェクトも破棄されません。

とはいえ前述のように、Itanium C++ ABIとWindows(MSVC)の実装では例外オブジェクトを保持しておくための領域を確保する方法が異なります。MSVC実装をはじめとするスタックのどこかに例外オブジェクトを構築する環境の場合、std::exception_ptrは必ずしもポインタのようになっておらず、std::exception_ptr自身の領域内部に例外オブジェクトを保存している場合があります。

そのような環境の場合、std::exception_ptrを作成/コピーするたびに例外オブジェクトがコピーされ、例外オブジェクトは例外ハンドラの終わりでいつも破棄されその領域も解放されます。そのような場合でも、例外オブジェクトの寿命やstd::exception_ptrの扱いなどは一見同様であり、例外オブジェクト周りの仕様はそのような実装を想定されて設計されています。

ちなみに、std::current_exception()は当初から例外オブジェクト領域の自由度を認めるように規定されていたのですが、対になるstd::rethrow_exception()はそうなっていませんでした(例外オブジェクトのコピーを認めていなかった)。これはC++23にて修正され、MSVCをはじめとする処理系の実装が規格準拠の振る舞いとなるようにされました(C++20まではMSVC等の実装が規格違反になっていただけで、C++23で実装方法が変わるわけではない)。

throw式の直接のオペランドから例外ハンドラの引数までの間には、例外オブジェクトの型の決定及び例外ハンドラの型マッチングという2段階のフィルタが挟まっており、これによってスローした値とほぼ同じ型でしかキャッチできなくなっています。

例えば整数型の場合でも、他の場所とは異なり暗黙変換してキャッチするようなことにはならず、投げた直接の型(およびそのCV 参照付きの型)のハンドラでキャッチされます。

int main() {
  try {
    throw 10; // int型の値をスロー
  } catch (short) {
    std::cout << "catch short.\n";
  } catch (unsigned int) {
    std::cout << "catch uint.\n";
  } catch (long long) {
    std::cout << "catch long long.\n";
  } catch (unsigned long long) {
    std::cout << "catch ulong long.\n";
  } catch (int) {
    std::cout << "catch int.\n";  // ここにくる
  }
}

ここで、最後のハンドラの型はintの他に、int&, const int, const int&のどれに変えてもキャッチされます。しかし、それ以外の型でキャッチすることはできません。

これと同様に、prvalue0をスローする場合に暗黙にnullptrに変換されたりしません。

int main() {
  try {
    throw 0;
  } catch (std::nullptr_t) {
    std::cout << "catch nullptr_t.\n";
  } catch (int*) {
    std::cout << "catch int*.\n";
  } catch (int) {
    std::cout << "catch int.\n";  // ここにくる
  }
}

一方、ハンドラの型マッチングで特別扱いされているため、nullptrをスローすると任意のポインタ型でキャッチできます。

int main() {
  try {
    throw nullptr;
  } catch (void*) {
    std::cout << "catch void*.\n";  // ここにくる
  }

  try {
    throw nullptr;
  } catch (int*) {
    std::cout << "catch int*.\n"; // ここにくる
  }

  try {
    throw nullptr;
  } catch (double*) {
    std::cout << "catch double*.\n";  // ここにくる
  }

  try {
    throw nullptr;
  } catch (std::nullptr_t) {
    std::cout << "catch std::nullptr_t.\n"; // ここにくる
  } 
}

同様に、例外オブジェクトの型Tに対してその(publicで曖昧でない)基底クラスDのオブジェクトor参照の場合も特別扱いされているため、自然に受ける事ができます。

int main() {
  try {
    throw std::runtime_error{"test throw"};
  } catch (const std::exception& ex) {
    std::cout << "catch std::exception : " << ex.what() << '\n';
  }
}

ただし、polymorphicな型がスローされているときに、その基底クラスを値でキャッチすると意図通りになりません。特に、ハンドラで抽象クラス型を値で受けようとするとコンパイルエラーになります。

// 抽象クラス型
struct A {
  virtual void f() = 0;
  virtual ~A() = default;
};

struct D : A {
  virtual void f() {
    std::cout << "D::f()\n";
  }
};


int main() {
  try {
    throw std::runtime_error{"test throw"};
  } catch (std::exception ex) {
    std::cout << "catch std::exception : " << ex.what() << '\n';  // what()は空
  }

  try {
    throw D{};
  } catch (A a) { // コンパイルエラー
    std::cout << "catch A \n";
  }
}

余談 : 決して選択されない例外ハンドラ

例外ハンドラの型として配列参照型と関数参照型を指定すると決してキャッチされないと指定されています。実際、例外オブジェクトの型が配列型/配列参照型/関数参照型になることはなく、例外ハンドラの型マッチング時に配列参照/関数参照がマッチングするルールはありません。

ただし、これはGCC(12)ではそうなっていないようです。clangもバージョンによってはセグフォ起こしたりしています。

int main() {
  try {
    int array[1]{1};
    throw array;  
  } catch (const int(&)[1]) {
    std::cout << "catch array ref\n";  // GCC 12はこっち
  } catch (const int*) {
    std::cout << "catch\n"; // clang 17はこっち
  } catch (...) {
    std::cout << "through\n";
  }
}
void f() {}

int main() {
  try {
    throw +f;  
  } catch (void(&)()) {
    std::cout << "catch function ref\n";  // GCC 12はこっち
  } catch (void(*)()) {
    std::cout << "catch\n"; // clang 17はこっち
  } catch (...) {
    std::cout << "through\n";
  }
}

関数ポインタはともかく、配列を投げるときの例外オブジェクトの型はint*に減衰されているため、その要素がきちんと例外オブジェクトに保存されるには規格の規定だけでは足りない気もします。あるいは、例外オブジェクトの型とはハンドラのマッチングのための表層的なものでしかなく、実際の例外オブジェクトの型とは異なるということなのかもしれません。力尽きたので、この謎の真相はどなたかにお任せします・・・

参考文献

この記事のMarkdownソース

[C++]iter_const_reference_tの型の決定について

先日zennに投降した、views::as_constについての記事を書いているときに調べた、const_iteratorの要素型(参照型)の決定過程に関するメモです。

以下、views::as_constに関しては知っているものとして説明しません(この記事ないし他の解説をご覧ください)。

また、上記記事及び以降でも、イテレータの間接参照結果の事を指してイテレータの要素だとか要素型だとか言っていますが、それは正しくは要素の参照あるいは参照型(iter_reference_t)の事です。本来の意味の要素型(iter_value_t)とは異なり、views::as_constconst_iteratorは要素の間接参照結果をconst化するのであって要素型そのものをconst化するわけではありません。

views::as_constconstant_range

views::as_constは入力範囲の要素がすでにconstになっているかどうかを判定してからそうなっていない場合に何か作業を行います。その判定に関わっているのはviews::as_constと共にC++23で追加されたstd::ranges::constant_rangeコンセプトです。

// constant_rangeの定義
template<class T>
concept constant_range = 
  input_range<T> && 
  constant-iterator<iterator_t<T>>;

constant_rangeの主要な部分を担っているのがconstant-iteratorというコンセプトです。これは説明専用の(規格文書内で規定のために使用される)もので、次のように定義されています。

template<class It>
concept constant-iterator =
  input_iterator<It> && 
  same_as<iter_const_reference_t<It>, iter_reference_t<It>>;

constant-iteratorコンセプトの主要な部分は2つ目の制約式で、この制約式は入力のイテレータItに対して、そのiter_const_reference_titer_reference_tが一致することを求めています。iter_reference_tイテレータの間接参照(*)の直接の結果型のことで、どうやらそれがconstになっていればこの制約式を満たせるように見えます。

このiter_const_reference_tviews::as_constと共にC++23で追加されたエイリアステンプレートで、次のように定義されています

template<indirectly_readable It>
using iter_const_reference_t =
  common_reference_t<const iter_value_t<It>&&, iter_reference_t<It>>;

イテレータItに対して、その要素型(std::iter_value_t)をconst参照化したものと参照型のcommon_referenceを求めています。

common_referenceの決定過程は複雑ですが、雰囲気的にはこの結果は常にconstが付いた型になりそうに見えます。とりあえず今はそれを認めることにして、iter_const_reference_t<I>は常にconstであることを仮定しておきます。

std::ranges::cbegin()/cend()

views::as_constの入力範囲がconstant_rangeではない場合、views::as_constは次にその型を何とかこねくり回して要素のconst化が達成できないかを試行します。それが叶わない場合、入力範囲をas_const_viewに渡して返すことで要素型のconst変換を行います。

views::as_constas_const_viewを使用する場合、そのイテレータstd::ranges::cbegin()から取得されます。ranges::cbegin()C++23で確実に要素がconstになっているイテレータを返すように改修されており、型Tの式Eとそれの評価結果の左辺値をtとしてranges::cbegin(E)のように呼ばれた時次のようなことを行います

  • enable_borrowed_range<remove_cv_t<T>> == falseならば、ill-formed
  • そうではない場合、式Uranges​::​begin(possibly-const-range(t))として、const_iterator<decltype(U)>(U)を返す

possibly-const-rangeは説明専用の関数であり、次のように定義されています

template<input_range R>
constexpr auto& possibly-const-range(R& r) {
  if constexpr (constant_range<const R> && !constant_range<R>) {
    return const_cast<const R&>(r);
  } else {
    return r;
  }
}

ここで行われていることは、入力範囲Rについて入力範囲を単にconst化すればその要素もconst化する(const Rconstant_rangeとなる)場合はそうして、そうでない場合、及びRが既にconstant_rangeである場合は入力をそのまま返す、という事をしています。

ranges::cbegin()は、こうして返された範囲オブジェクトからranges::begin()によってイテレータを取得し、それをstd::const_iteratorに通して返します。

ranges::cend()はこの手順内のranges::begin()ranges::end()に置き換えたことを行い、最後にstd::const_sentinelを通して返します。

std::const_iterator/const_sentinel

std::const_iteratorエイリアステンプレートであり、入力の型Iによって次のどちらかの型を返します

  • Iconstant-iteratorならばI
  • そうでないならば、basic_const_iterator<I>

ここで出てくるconstant-iteratorコンセプトは、constant_rangeで使用されていたものと同じものを指しています。すなわち、Iの要素型がconstではない場合にのみIstd::basic_const_iteratorにラップして返します。

std::const_sentinelエイリアステンプレートであり、入力の型Sによって次のどちらかの型を返します

  • Sinput_iteratorならばconst_iterator<S>
  • そうでないならば、S

これはどちらも、C++23でviews::as_constと共に導入されたものです。

std::basic_const_iterator

std::basic_const_iteratorC++23でviews::as_constおよびranges::cbegin()のために追加されたイテレータラッパであり、入力イテレータの間接参照結果をconst化する事だけを行います。

前述のように、イテレータIに対していつもstd::const_iterator<I>を使用するようにすれば、std::basic_const_iteratorstd::basic_const_iteratorでラップするような二重const化を回避してイテレータ要素のconst化を達成できます。

std::basic_const_iteratorの見どころはほぼそのoperator*のみで、それは内部のイテレータitに対してreturn static_cast<reference>(*it)を返します。referencestd::basic_const_iterator入れ子型として説明専用として定義されるもので、次のように定義されています

template<input_iterator Iterator>
class basic_const_iterator {
  ...
  
  // 説明専用型referenceの定義
  using reference = iter_const_reference_t<Iterator>;
  
  ...
};

結局ここでも、iter_const_reference_tが出てきます。

std::iter_const_reference_t

こうして、役所もびっくりのたらい回しの果てに、views::as_constの要素型の決定にはstd::iter_const_reference_tというエイリアステンプレートが深く関与していること分かりました。views::as_constの分岐を考えると、これが直接介さない場合でもviews::as_constの結果のviewの要素型(参照型)は入力範囲のイテレータ型をstd::iter_const_reference_tに通して得られた型と同じになるはずです(たぶん)。

従って、views::as_constの結果範囲の要素型はstd::iter_const_reference_tがどう振舞うかを調べればわかることになります。std::iter_const_reference_tの定義は次のようになっており

template<indirectly_readable It>
using iter_const_reference_t =
  common_reference_t<const iter_value_t<It>&&, iter_reference_t<It>>;

その決定には2つの型のcommon_referenceが計算されています。

その1つ目の型const iter_value_t<It>&&では、iter_value_t<It>が参照型ではない場合に結果は必ずconstになり、かつ常に参照型になります。とはいえiter_value_t<It>は通常修飾なしの型(prvalue)であるはずで、std::indirectly_readable_traitsをよく見るとremove_cvされるうえに参照型だとvalue_typeが定義されないことが分かるため、いつも修飾なしの型であるとします(もう一つiterator_traitsの経路だと必ずしもそうではありませんが)。すると、const iter_value_t<It>&&とはいつもconst右辺値参照になります。

2つ目の型iter_reference_t<It>イテレータの関節参照の結果型であり、これは

  • 修飾なしの型(prvalue
  • 参照型
    • 左辺値参照(&
    • 右辺値参照(&&
  • 上2つのconst修飾

のいずれかであるはずです(volatileは無視で・・・)。

それらの型の間のcommon_referenceの決定は複雑ですがこれらの仮定を用いるとある程度求めやすくなります。特に、1つ目の型は常にconst右辺値参照型なので、問題となるのは2つ目の型が参照型かどうかです。std::common_reference<const T&&, U>とすると、この結果は次のどちらかになりそうです(あまり自信がない・・・)

  • Uが参照型(U&/U&&) : decltype(false ? declval<const T&(&)()>()() : declval<const U&(&)()>()())
  • UprvalueU) : decltype(false ? declval<const T&&(&)()>()() : declval<U(&)()>()())

とはいえこの型がどうなるかも難しいものがあり(そもそもパースがムズカシイ)、Uconstがついてると少し正しくない部分があるなどやはり難しいものがあります。なので、簡単なテスターを作って実際の振る舞いを確かめてみます。

template<typename T>
using test = std::common_reference_t<const std::remove_cvref_t<T>&&, T>;

これを使うと、std::iter_const_reference_tの型をイテレータ型を介さず直接調べることができます。

int main() {
  static_assert(std::same_as<test<int&>, const int&>);
  static_assert(std::same_as<test<int&&>, const int&&>);
  static_assert(std::same_as<test<const int&>, const int&>);
  static_assert(std::same_as<test<int>, int>);
  static_assert(std::same_as<test<const int>, int>);
}

イテレータIの参照型(iter_reference_t<I>)をT(+修飾)とし、型Tに対して特にbasic_common_reference特殊化が行われていないとして、これによるとTによってstd::iter_const_reference_t<I>は次のようになります

iter_reference_t<I> iter_const_reference_t<I>
T& const T&
T&& const T&&
const T& const T&&
const T&& const T&&
T T
const T T

Tが参照型である場合は適切にconst化されていますが、Tprvalueである場合はそうなってはいません。それはまあ当然というか、prvalueconst参照でラップしたらそれはダングリング参照になりますし、prvalueconstを付加することにはほぼ意味がありません。

これは一般の型Tに対しての結果ですが、意図的にcommon_referenceが(basic_common_referenceによって)カスタムされている一部の型が標準ライブラリにも存在し、それを使用するようなrange型ではその要素型と参照型の景色が少し異なっていたりします。たとえば

  • std::vector<bool>
    • range_value_t : bool
    • range_reference_t : std::vector<bool>::reference
  • views::zipとそのファミリ
    • range_value_t : std::tuple<T, U>
    • range_reference_t : std::tuple<T&, U&>
  • views::enumrate
    • range_value_t : std::tuple<D, T>
    • range_reference_t : std::tuple<D, T&>

などがあります。

ここでのT, Uviews::zipviews::enumarateの入力範囲のrange_value_t/range_reference_t/range_difference_tによって変化するためそのプレースホルダーとします。T, Uは任意の型の可能性がありますがここでは参照修飾は考えないことにして、Dprvalueの整数型です。

これらの型の場合は、値型(range_value_t)・参照型(range_reference_t)・const要素型(range_const_reference_t)はそれぞれ以下のようになります

range range_value_t<I> range_reference_t<I> range_const_reference_t<I>
std::vector<bool> bool std::vector<bool>::reference bool
const std::vector<bool> bool bool bool
views::zip std::tuple<T, U> std::tuple<T&, U&> std::tuple<const int&, const double&>
views::enumrate std::tuple<D, T> std::tuple<D, T&> std::tuple<D, const T&>

range_const_reference_titer_const_reference_trange版で、iter_const_reference_t<iterator_t<R>>として定義されています。

zipenumrateの結果をみると想像がつくかもしれませんが、std::tuple<Ts...>std::tuple<Us...>common_referenceとは、std::tuple<std::common_reference_t<Ts, Us>...>のように、tuple要素型の対応する型同士のcommon_referenceを求める形になります。なので、std::tupleの他のケースはその要素型について先程の一般の型に対する結果を参照すると簡単に求められます。また、その場合はtuple自身のconst/参照修飾は無視されます。

iter_const_reference_tはこのように、とても巧妙に入力イテレータ型の要素型をそれに応じて適切にconst化します。しかも、prvalueの場合は余計なことをしないなど、本当によくできすぎていることが分かります。

参考文献

この記事のMarkdownソース

[C++]WG21月次提案文書を眺める(2023年04月)

文書の一覧

SG22のWG14からのものを除いて、全部で41本あります。

N4940 WG21 2022-11 Kona Minutes of Meeting V2

2022年11月7-12日にハワイのKonaで行われた、WG21全体会議の議事録。

N4933の改訂版です。

N4941 INCITS C++/WG21 Agenda: 6-11 February 2023, Issaquah, WA USA

2023年2月6-11日にアメリカのIssaquahで行われた、WG21全体会議の全体予定表。

N4942 WG21 2023-01 Admin telecon minutes

2023年1月に行われた、WG21管理者ミーティングの議事録

N4943 WG21 February 2023 Issaquah Minutes of Meeting

2023年2月6-11日にアメリカのIssaquahで行われた、WG21全体会議の議事録。

おそらく、初日と最終日に行われた全体の会議の議事録です。

N4944 Working Draft, Standard for Programming Language C++

C++23のワーキングドラフト第9弾。

N4945 Editors' Report - Programming Languages - C++

↑の変更点をまとめた文書。

2月のIssaquah会議で採択された提案とコア言語/ライブラリのIssue解決が適用されています。

P0876R13 fiber_context - fibers without scheduler

スタックフルコルーチンのためのコンテキストスイッチを担うクラス、fiber_contextの提案。

以前の記事を参照

このリビジョンでの変更は

  • コンストラクタに渡す関数オブジェクトをdecay-copyするようにした
  • std::span<std::byte, N>を受け取るコンストラクタ(使用するメモリ領域を指定する)の引数をstd::span<std::byte>に変更
    • また、デリータを指定できるようにした
  • コンストラクタの例外条件について追記
  • 空ではない(何か処理が進行中の)fiber_contextが破棄された場合、std::terminate()を呼ぶことを規定
  • resume_with()を呼ぶと、すぐにempty() == trueとなることを明確化
  • 文言の簡素化のために、fiber_context::stateという説明専用メンバを導入
  • concurrency_v2名前空間を削除
  • Equivalent toという言葉をAs-ifに変更
  • 事前条件と適格要件を明確化

などです。

P1144R7 std::is_trivially_relocatable

オブジェクトの再配置(relocation)という操作を定義し、それをサポートするためのユーティリティを整える提案。

以前の記事を参照

このリビジョンでの変更は

  • P2786R0の登場で提起された疑問点を追記
  • std::vector<T>::insert()の実装のための、std::uninitialized_relocate_backwardの追加
  • trivially relocatableな型の要件から、ムーブ構築可能と破棄可能を削除
  • relocationによって可能となる最適化について、既存の在野のライブラリ機能と比較
  • 2月のIssaquah会議での投票結果を追記
  • 背景や議論などの説明を削除
  • EASTLのrelocationは少し意味が異なっていたので、言及を削除

などです。

2月のIssaquah会議では、別にほぼ同様の概念を提案するP2786R0が提出されレビューされました。この提案の著者の方とそちらの著者の方は、合同で2つの提案をマージした提案を準備しているようです。

P1673R12 A free function linear algebra interface based on the BLAS

標準ライブラリに、BLASをベースとした密行列のための線形代数ライブラリを追加する提案。

以前の記事を参照

このリビジョンでの変更は多岐に渡りますが、ほとんどが文章そのものもしくは提案する文言の調整や修正です。それ以外のところでは

  • 実装経験の追記
  • 機能テストマクロの追加(__cpp_lib_linalg
  • transposedは読み取り専用mdspanを返さなくなった
  • 値渡しのパラメータからconstを削除
  • vector_norm2vector_two_normへ変更
  • symmetric_matrix_rank_k_updatehermitian_matrix_rank_k_updateにはalpha scalingパラメータを取らないオーバーロードが追加された
  • {symmetric,hermitian,triangular}_matrix_{left,right}_product{symmetric,hermitian,triangular}_matrix_productへ変更
    • パラメータの順序によって、left,rightを識別するようにした
    • これによって、triangular_matrix_product(in-place right productの場合)は、(入)出力パラメータが例外的に最後の引数に現れなくなった
  • [in]out-{matrix,vector,object}では、要素型がconstであるかをチェックする代わりに、要素型が参照型に代入可能(可変参照で束縛可能)であるかをチェックするようにした
  • 複素数に関する操作について、*-if-neededのような名前の関数を追加して、カスタムの複素数型を使用可能なように調整
  • std::absが符号なし整数型に対して定義されていないことに対処するために、説明専用のabs-if-neededを追加し、std::absの代わりに使用
  • in-placeで上書きする三角行列と行列の(左/右の)積の場合、その関数名のleft/rightを復元し、入出力パラメータを常に末尾配置する。
    • これによって、in-placeの場合にのみ、triangular_matrix_left_producttriangular_matrix_right_productが復活した(4つ上の変更の再修正)

などです。

この提案はLEWGのレビューを一旦終えて、LWGに転送するための投票待ちをしています。

P1885R11 Naming Text Encodings to Demystify Them

システムの文字エンコーディングを取得し、識別や出力が可能なライブラリを追加する提案。

以前の記事を参照

このリビジョンでの変更はLWGフィードバックを判定したことです(変更が大量ですが、設計の変更はないはずです)。

この提案は、LWGのレビューを終えて次の全体会議で投票にかけられる予定です。

P2022R1 Rangified version of lexicographical_compare_three_way

std::lexicographical_compare_three_wayのRange版を追加する提案。

以前の記事を参照

このリビジョンでの変更は、筆者の方による実装コードへのリンクを追記した事です。

P2287R2 Designated-initializers for base classes

基底クラスに対して指示付初期化できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、以前に提案していた基底クラスを指定して指示付初期化を行う構文を削除した事です。ただし、基底クラスの非静的メンバを指定するR1で追加された構文は引き続き提案されています。

struct A {
  int a;
};

struct B : A {
  int b;
};

int main() {
  // R0で提案されていた構文、R2(このリビジョン)で削除
  B b1{:A = {.a = 1}, b = 2};
  B b2{:A{.a = 1}, b = 2};
  B b3{:A{1}, .b{2}};

  // R1で追加され、R2でも可能な構文
  B b4{.a = 1, .b = 2};
  B b5{.a{1}, .b{2}};
}

このリビジョンではb4, b5の形式のみがサポートされ、基底クラスを指名する形のb1, b2, b3の形式は提案されていません。

P2407R3 Freestanding Library: Partial Classes

一部の有用な標準ライブラリのクラス型をフリースタンディング処理系で使用可能とする提案。

以前の記事を参照

このリビジョンでの変更は

  • // freestanding-delete// freestanding-deletedへ変更
  • 一貫性のために、bad_optional_accessを追加
  • string_viewstarts_with/ends_withの文言を変更
  • // freestanding-deletedの使用例を追加
  • ヘッダへの指定のための、// mostly freestandingを追加

などです。

P2447R3 std::span over an initializer list

std::spaninitializer_listを受け取るコンストラクタを追加する提案。

以前の記事を参照

このリビジョンでの変更は、主著者の変更と機能テストマクロを削除したことなどです。

P2530R3 Hazard Pointers for C++26

標準ライブラリにハザードポインタサポートを追加する提案。

以前の記事を参照

このリビジョンでの変更は、タイトルの変更とLWGレビューのフィードバックを反映した事です。

P2545R4 Read-Copy Update (RCU)

標準ライブラリにRead-Copy-Update(RCU)サポートを追加する提案。

以前の記事を参照

このリビジョンでの変更は、タイトルの変更とLWGレビューのフィードバックを反映した事などです。。

P2630R3 Submdspan

std::mdspanの部分スライスを取得する関数submdspan()の提案。

以前の記事を参照

このリビジョンでの変更は

  • 機能テストマクロの追加
  • 集成体型に関する文言を修正
  • sub_map_offsetを使用される前に定義するように移動
  • integral-constant-likeintegral_constantの代わりに使用

などです。

P2690R1 Presentation for C++17 parallel algorithms and P2300

P2500(以前のP2690R0)の紹介スライド

P2500(P2690R0)については以前の記事を参照

どうやら、P2690R0は間違った番号を使用して公開されてしまったようで、P2690としてはこのスライドを公開しP2690R0はP2500R0として公開する予定だったようです。そのためP2690R0はP2500に修正され、その紹介スライド(このスライド)はP2690R1として公開されたようです。

このスライドでは、P2500(P2690R0)の内容をSG1やLEWGのメンバに簡単に解説するものであり、主にAPIの概要やその意図などを解説しています。

P2746R1 Deprecate and Replace Fenv Rounding Modes

浮動小数点環境の丸めモード指定関数std::fesetround()を非推奨化して置き換える提案。

以前の記事を参照

このリビジョンでの変更は、

  • 説明文書の修正
  • WG14からのコメントの反映
  • フリー関数アプローチに焦点を当て、より具体的なAPIの概要を追記

などです。

R0ではcorrectly_rounded<F>という浮動小数点数のラッパ型を追加して、そのメンバ関数として正しい丸めを行う関数を追加することを提案していました。

このリビジョンでは、それを浮動小数点数型と丸めモード指定を直接受けて、その丸めモードの下で正確な計算を行うフリー関数を追加する方向に切り替えています。

namespace std {
  // 以下のcr_*関数はIEC 60559に完全に適合しているかを返す
  template<floating_point F>
  constexpr bool conforms_to_iec_60559();

  // 指定された丸めモードの下で四則演算を行う

  template<floating_point F>
  constexpr F cr_add(F x, F y, float_round_style r = round_to_nearest);
  
  template<floating_point F>
  constexpr F cr_subtract(F x, F y, float_round_style r = round_to_nearest);
  
  template<floating_point F>
  constexpr F cr_multiply(F x, F y, float_round_style r = round_to_nearest);
  
  template<floating_point F>
  constexpr F cr_divide(F x, F y, float_round_style r = round_to_nearest);
  
  // 浮動小数点数型 G -> Fへの丸めを伴う変換
  // sizeof(F) >= sizeof(G) の時丸めは正確になる(はず
  template<floating_point F. floating_point G>
  constexpr F cr_cast(G x, float_round_style r = round_to_nearest);
 
  // 浮動小数点数文字列をFの値に変換する
  // どのような文字列がサポートされるかは実装定義、サポートされない場合は例外をスローする
  template<floating_point F> consteval F cr_const(string s);

  // 指定した丸めモードの下で平方根を計算する
  template<floating point F>
  constexpr F cr_sqrt(F x, float_round_style r = round_to_nearest);
}

P2752R1 Static storage for braced initializers

std::initializer_listの暗黙の配列がスタックではなく静的ストレージに配置されるようにする提案。

以前の記事を参照

このリビジョンでの変更は、マングリングに関する議論を削除した事(問題とならなかったため)、GCC-fmerge-all-constantsに関する議論を追記したことなどです。

この提案の内容は、GCCがすでに-fmerge-all-constantsで有効化される独自拡張の一部として実装済みだったようです。

P2757R1 Type checking format args

std::format()のフォーマット文字列構文について、幅/精度の動的な指定時の型の検証をコンパイル時に行うようにする提案。

以前の記事を参照

このリビジョンでの変更は

  • basic_format_parse_contextコンストラクタの変更(削除)を元に戻し、check_dynamic_spec_arithmeticを削除
    • "arithmetic"型にboolcharを含めるようにした
    • これによって、check_dynamic_spec_integral()がその機能を包含する

などです。

この提案はLEWGのレビューを終えて、LWGに転送するための投票待ちをしています。

P2780R0 Caller-side precondition checking, and Eval_and_throw

契約プログラミングのEval_and_throwモード実装のための動作(実装)モデルの提案。

P2698R0では、契約違反が起きた時に無条件終了するのではなく例外を投げて継続するモードであるEval_and_throwモードが提案されています。

このモードで問題となっているのはnoexceptとの兼ね合いで、Eval_and_throwモードでは契約がなされている関数は全て例外を投げる可能性があるため、その関数の(あるいはその関数に対する)noexceptがどうなるのかが問題となっています。

この提案は、契約条件(事前条件)のチェックが関数内部ではなく関数呼び出し側で行われるという動作モデルによって、この問題を解決しようとするものです。

Eval_and_throwモードとnoexceptの問題は、契約条件の評価がその関数の内側にあるという実行モデルに基づいています。そこで、契約条件(事前条件)のチェックは関数の呼び出し側で、関数呼び出しの前で行われるようにして、契約条件の評価と関数呼び出しを分離します。これによって、noexceptの扱いはこれまで通りとなり、noexcept指定されている関数で契約指定を行うことができるようになります。

この動作モデルでは、呼び出される側の準備や協力がなくても、完全に呼び出し側の責任範囲で契約のチェックを行うことができます。すなわち、呼び出される側の関数実態が別の翻訳単位にある時でも、翻訳単位間で何かを共有する必要はなく、呼び出し側で契約評価の有効/無効を切り替える時でも同じバイナリを使用し続けることができ、契約機能の状態によるABIの変化はありません。これによって、プレビルドバイナリを配布するようなライブラリでは契約機能の状態に応じた数のバイナリを配布するのではなく従来通りに1つのバイナリだけを配布し、そのAPIにおける契約の有効/無効はライブラリ使用者の任意で切り替えることができるようになります。

このことは、単にEval_and_throwモードの実装という枠を超えて契約機能全体にとって有用である可能性があります。この提案が採用されないにしても、契約機能がABIに影響を与えないように実装されることは非常に重要だと筆者の方は述べています。

この提案の動作モードは、単純なコードで示すと次のようなものになります

// 関数呼び出しは
f();

// このように書き換えられて実行される
((precond() ? nop() : violation()), f());

precond()とは事前条件チェックの全体であり、nop()は何もしないダミーの関数で、violation()は違反ハンドラの呼び出しです。violation()は評価されると、Eval_and_abortモードではstd::terminate()を呼び出し、Eval_and_throwモードでは例外を送出します。

このコードでは、violation()が呼び出されるとf()の呼び出しには到達しないため、violation()が例外を投げるかどうかはf()noexcept性に影響を与えません。そのため、noexcept関数でも契約指定を行うことができます。また、このような実装は、コンパイラのフロントエンドだけで実装することができます。

この提案は、MVPの一部あるいはEval_and_throwモードのためというわけではなく、契約機能そのものに対して次のことを提案しています

  1. 呼び出される関数の事前条件チェックを、現在の翻訳単位内のコードによって可能とする
  2. オーバーロード解決済の関数呼び出しのみチェックすることを可能とするために、対象となる関数を名前を指定する関数とメンバ関数に限定する
  3. 現在の翻訳単位内での契約アサートのチェックを可能とする
  4. 現在の翻訳単位内での事後条件評価を可能としない
  5. 事前条件が何回評価されるかは保証されない

また、Eval_and_throwモードを採用する場合でも、それを有効にするとこれらのことが有効となり、これ以上の何かが有効にはならないことを提案しています。

P2806R1 do expressions

値を返せるスコープを導入するdo式の提案。

以前の記事を参照

このリビジョンでの変更は

  • 最後の値を暗黙的に戻り値とすることと明示的なreturnについての議論を追加
  • リフレクションについての議論を追加
    • リフレクションのようなコードインジェクション機能があってもこの機能は必要か?という問いに対する回答
  • 文法の修正

などです。

最後の値を暗黙的に戻り値とすることとは、do式のスコープで最後に表れた値を暗黙的に戻り値とみなすことです。つまり、Rustなどで一般的に行われていることをdo式限定でできないか?ということです。

暗黙`return` `do return`
auto foo(int i) -> std::expected<int, E>

auto bar(int i) -> std::expected<int, E> {
    int j = do {
        auto r = foo(i);
        if (not r) {
            return std::unexpected(r.error());
        }
        *r // <== NB: no semicolon
    };

    return j * j;
}
auto foo(int i) -> std::expected<int, E>

auto bar(int i) -> std::expected<int, E> {
    int j = do {
        auto r = foo(i);
        if (not r) {
            return std::unexpected(r.error());
        }
        do return *r;
    };

    return j * j;
}

単純な例では短くなりますが、C++の場合は早期returnができないためその有効性は限定的です。(do return必要性の有無を排他的とすると)例えばdo式内のループから戻ることができません。それをサポートするにはさらにif式が必要となりますが、それはこの提案の設計にさらなる複雑さを加えることになります。

P2809R0 Trivial infinite loops are not Undefined Behavior

自明な無限ループを未定義動作ではなくする提案。

C++では副作用のない無限ループは未定義動作となり、たびたび話題になります。例えば次のようなコードはなぜかHello world!が出力されます。

#include <iostream>

int main() {
  while (true)
    ; 
}

void unreachable() {
  std::cout << "Hello world!" << std::endl;
}

これはC++11でスレッドを言語としてサポートした際に、並行プログラムにおける進行保証(forward progress guarantee)の一環としてメモリモデルとともに導入されたものです。同じことはC11でも行われ、C言語forward progress guaranteeを言語として定義していますが、そこではループの条件が定数式であるような自明な無限ループは未定義動作とはなりません。

forward progress guaranteeは並行プログラムが正しく進行することを保証するための枠組みであり、この制約がきつすぎると並行処理を行うソフトウェア及びそれを実行するハードウェアの設計を妨げてしまう可能性があります。C++における無限ループの扱いも、並行プログラム及びハードウェアの設計自由度を確保するための措置であるようです。

しかし、このことは逆にシングルスレッドのプログラムにおいては問題となり、ベアメタルのシステムやカーネルなど、低レベルのプログラムにおいてプログラムの進行を意図的に停止させておくことは一般的なイディオムであり、この問題はそれを妨げています。

コンパイラがこれを診断しそれをプログラマに伝えることは容易なはずですが、現在の実装はそれを行っておらず、上記例のように最適化に利用してしまっています。

この提案は、この問題を解決するために、ループ継続条件が定数式であるようなループをプログラマの意図的なものと解釈し、そのような自明なループはC++においても未定義動作として扱わないようにしようとするものです。ただし、これは次のことを変更するものではありません

  • 継続条件が定数式ではないループは、引き続き未定義動作となる
    • この種のループは終端保証による最適化の恩恵がある
  • gotosetjmp/longjmp、無限末尾再帰などの他の方法によるループの扱い
  • 並列・並行プログラムにおける進行保証(forward progress guarantee
    • 自明なループを持つ場合を除く

実際には、forward progress guaranteeの一環として無限ループが未定義動作となってしまったのは、C++11/C11時点の既存の慣行(特にコンパイラ実装)を反映したもののようで、必ずしも並行プログラムにおけるforward progress guaranteeを満たすためだけのものではなかったようです。

当時のコンパイラはループを変形する最適化を行うためにループが終了することを仮定していましたが、これは当時のC99の規定(ループは継続条件が0になると終了する)に違反していました。並行プログラミングを言語としてサポートしつつもその性能を妨げないために、C++11/C11ではこの慣行を標準化することでそれらの最適化を受け入れました。

ただ、ループが終了するかを判定することは通常停止性問題を解くことと同値でありそれは不可能であるため、この種の最適化の条件を「停止性問題を解く」から「観測可能な副作用を保つ」に変更することで扱いやすくするとともに、それをforward progress guaranteeに組み込むことで並行アプリケーションにおいてもそのような最適化を有効化したようです。

P2811R0 Contract Violation Handlers

P2811R1 Contract Violation Handlers

契約プログラミングに、ユーザー提供の違反ハンドラを許可する提案。

C++20で削除された契約プログラミング機能では、契約違反時の違反ハンドラを上書きすることでユーザーがカスタマイズすることができました。それによって、契約違反時の振る舞いをビルドモードとは無関係にカスタマイズすることができ、例えば次のような事に利用することを意図していました

  • 契約違反時の報告方法や形式のカスタマイズ
  • 契約違反時のセマンティクス管理
    • 即終了する、例外を投げる、longjmpによる終了、無視して継続、など

現在のContract MVP仕様では、違反ハンドラはシステムが提供するデフォルトのものから置き換えることはできず、契約違反時の振る舞いもプログラム終了のみとなっています。すなわち、現在のMVPは契約条件チェックのセマンティクスの制御がビルドモードによって担われており、それも現状は選択肢が1つしかありません。

契約違反時の振る舞いの最適な選択はユーザーによって異なり、違反時の振る舞いをカスタマイズすることができる移植可能で一貫した方法を提供することは、ほとんどのユーザーによって有益であり、契約プログラミング機能の有用性を大きく高めることになります。

ただし、C++20契約機能で揉めたように、契約違反時に任意のコードを実行することにはリスクがあり、カスタムの違反ハンドラで行える有用なこととバランスを取る必要があります。それでも、契約違反時の処理をカスタムできることは一般的に必要であり、殆どの場合にはプラスに働きます。

この提案は、現在のMVP仕様に対して次の2点の変更を加えることで、違反ハンドラのカスタマイズをサポートできるようにしようとするものです

  1. 契約違反時のプロセスの一部として呼び出される関数(違反ハンドラ)をユーザーが提供する能力を追加する
    • Eval_and_abortモードでは、違反ハンドラが終了した後でプログラムを終了する
  2. デフォルトの違反ハンドラの動作について、実装可能ならば採用することが望ましい推奨事項を作成する

ただし、特殊なプラットフォームにおいては違反ハンドラをカスタムすること(契約違反時に任意コードを実行可能であること)は受け入れ難いセキュリティリスクとみなされる場合があるため、違反ハンドラのカスタムは条件付きサポートとし、そのようなプラットフォームにおいて違反ハンドラをカスタムする場合はエラーとすることを提案しています。

違反ハンドラはグローバルモジュールに属する::handle_contract_violation()という関数を定義することでカスタムします。

// 違反ハンドラの宣言
void handle_contract_violation(const std::contracts::contract_violation&);

この関数は[[noreturn]]であってもよく、noexceptの指定も任意です。引数のstd::contracts::contract_violationは契約違反が発生した状況についての情報を持つクラス型で、次のように定義されています

// <contract> ヘッダで定義
namespace std::contracts {

  enum class contract_violation_detection_mode : /*unspecified*/ {
    unknown,
    predicate_false,
    predicate_exception,
    predicate_undefined_behavior
  };

  enum class contract_semantic : /*unspecified*/ {
    ignore,
    enforce
  };

  enum class contract_kind : /*unspecified*/ {
    pre,
    post,
    assert
  };

  class contract_violation {
  public:

    // 破られた契約の条件式のテキスト表現 
    const char* comment() const noexcept;

    // 契約違反の起こり方
    contract_violation_detection_mode detection_mode() const noexcept;

    // 破られた契約の種別
    contract_kind kind() const noexcept;

    // 違反を起こした場所の情報
    source_location location() const noexcept;

    // ビルドモードに関する情報
    contract_semantic semantic() const noexcept;
  };
}

contract_violationクラスはABI互換を保つように実装されることを意図しています。

違反ハンドラが提供されない場合、実装はデフォルトの違反ハンドラを使用し、それは有用な診断情報をstderrなどに出力するものであることを推奨としています。

ユーザー提供のコールバック(違反ハンドラ)を介して契約違反時の振る舞いをカスタマイズすることは、他の同等のアサーションの実装において一般的かつ十分にテストされたアプローチであり、(筆者の方の)Bloombergにおいても2004年に投入されて以降使用され続けているようです。

また、このアプローチはEWG/LEWGのレビューを通過した上でC++20契約機能に組み込まれており、その後削除されたのは違反ハンドラカンスタマイズの有用性や設計とは関係がなく、C++20契約機能全体の設計によるものだったとのことです。

P2817R0 The idea behind the contracts MVP

Contracts MVPと呼ばれるものを解説する文書。

Contracts MVPとは、C++20で最終的に削除されたContracts仕様から物議を醸していない基礎的な部分を抽出した最小のContracts仕様です。ここには事前/事後条件とアサーションのための注釈構文とそのセマンティクスが含まれています。

これはC++契約プログラミング機能の全てではなく、始まりとなるもので、合意が取れている部分からインクリメンタルに機能を拡張していくための基盤となることを目指すものです。

この文書は、そのようなContracts MVPの背景や方針、考え方などについてまとめられたものです。

P2818R0 Uniform Call Syntax for explicit-object member functions

明示的オブジェクトパラメータを持つ関数をフリー関数呼び出し構文によって呼び出し可能とする提案。

この提案は一様関数呼び出し構文(Uniform Function Call Syntax : UFCS)の一種として提案されています。UFCSはC++に対してこれまで何度か提案されてきており、その必要性は次のようにまとめられます

  • ジェネリックコードでは、より汎用的であるためフリー関数呼び出し(f())が好まれる
  • IDEユーザーは、 オートコンプリートが効きやすくなるためメンバ関数呼び出し(o.f())を好む
  • より多くの選択肢があれば、より多くの文脈でidomaticなコードを書くことができるようになる

明示的オブジェクトパラメータとはいわゆるDeducing thisと呼ばれる機能の事です。明示的オブジェクトパラメータを持つ関数はメンバ関数としてしか呼び出すことができません。

この提案は、明示的オブジェクトパラメータを持つ関数にfriendとマークすることで、フリー関数呼び出し構文からでも呼び出し可能とするものです。

struct S {
  // Deducing this + friend
  friend int f(this S) {
    return 42;
  }
};

int g() {
  S s{};

  s.f();  // OK, returns 42 (これは現在の振る舞い
  f(s);   // OK, 同上 (この提案の振る舞い
  (f)(s); // Error; fはADLのみで発見される
}

// 前方宣言の扱いについて
int f(S);    // Error, int f(S) conflicts with S::f(S)
int f(S) {}; // Error, int f(S) conflicts with S::f(S)

int f(int);  // OK

この提案による明示的オブジェクトパラメータを持つ関数のfriend宣言は、メンバ関数メンバ関数でありながらHidden friendsでもあるような状態にしています。

明示的オブジェクトパラメータを持つ関数はそもそもフリー関数に近いものであり、実際の扱いもほとんどフリー関数と同じ扱いをされますが、呼び出し周りはあくまでメンバ関数であるかのように扱われています。この提案では、明示的オブジェクトパラメータを持つ関数宣言にfriendを追加することで、見た目通りのフリー関数(Hidden friends)としての性質を有効化します。

また、この提案による変更は、明示的オブジェクトパラメータを用いた拡張メソッドのような将来の提案を妨げないことも意識されています。

P2819R0 Add tuple protocol to complex

std::complextupleインターフェースを追加する提案。

現在のstd::complex.real().imag()によってそれぞれ実部と虚部を取得/設定することができますが、この関数は参照ではなく値を返します。この関数を変更することはABI破壊になるため実質不可能であり、実部と虚部の値への参照を取得する唯一の方法は参照もしくはポインタにreinterpret_castすることです(このキャストは安全であることが保証されています)。しかし、reinterpret_castが定数式で禁止されているため、定数式でそれを行う方法はありません。

この提案は、std::complexにタプルインターフェースを追加することで、後方互換性を維持しつつより直感的かつ簡易な方法によって、複素数値のそれぞれの値の参照を取得できるようにしようとするものです。

数学的にも、複素数 \mathbb{C}はベクトル空間としての \mathbb{R}^2と同型であり、 \phi:\mathbb{C} \to \mathbb{R}^2の対応として \phi(a + bi) = (a, b)のような同型写像があります。これによって、複素数を実数2つからなるタプルだと思うことの理論的根拠が与えられます。

簡単な例

現在 この提案
std::complex<double> c{...};
auto& [r, i] = reinterpret_cast<double(&)[2]>(c);
std::complex<double> c{…};
auto& [r, i] = c;

実部と虚部を入れ替える例

現在 この提案
template<typename T>
constexpr
auto swap_parts(complex<T> c) -> complex<T> {
  if not consteval {
    auto & [r, i]{reinterpret_cast<double(&)[2]>(c)};
    swap(r, i);
  } else {
    // reinterpret_castが定数式で使用できないためのフォールバック
    const auto r{c.real()};
    const auto i{c.imag()};
    c.imag(r);
    c.real(i);
  }
  return c;
}
template<typename T>
constexpr
auto swap_parts(complex<T> c) -> complex<T> {
  auto& [r, i]{c};
  swap(r, i);
  return c;
}

さらに、これは将来のパターンマッチングでも有効である可能性があります。

現在 この提案
complex<double> c{…};

// P1371R3のパターンマッチング
inspect(reinterpret_cast<double(&)[2]>(c)) {
  [0, 0] => { cout << "on origin"; }
  [0, i] => { cout << "on imaginary axis"; }
  [r, 0] => { cout << "on real axis"; }
  [r, i] => { cout << r << ", " << i; }
};

// P2392R2のパターンマッチング
inspect(reinterpret_cast<double(&)[2]>(c)) {
  is [0, 0] => cout << "on origin";
  is [0, _] => cout << "on imaginary axis";
  is [_, 0] => cout << "on real axis";
  [r, i] is _ => cout << r << ", " << i;
}
complex<double> c{…};

// P1371R3のパターンマッチング
inspect(c) {
  [0, 0] => { cout << "on origin"; }
  [0, i] => { cout << "on imaginary axis"; }
  [r, 0] => { cout << "on real axis"; }
  [r, i] => { cout << r << ", " << i; }
};

// P2392R2のパターンマッチング
inspect(c) {
  is [0, 0] => cout << "on origin";
  is [0, _] => cout << "on imaginary axis";
  is [_, 0] => cout << "on real axis";
  [r, i] is _ => cout << r << ", " << i;
}

P2821R0 span.at()

std::span.at()メンバ関数を追加する提案。

.at()メンバ関数は、メモリ連続性を持つ(contiguousな)コンテナが持つ要素アクセス関数で、添字演算子[]が境界チェックを行わない(範囲外アクセスは未定義動作)のに対して境界チェックを行う(範囲外アクセスはstd::out_of_range例外)ものです。

std::vectorstd::array等、メモリ連続性を持つ他の標準コンテナは全て[]at()の両方を持っていますが、std::span[]しかありません。

在野のspan実装ライブラリにおいては.at()メンバ関数がその実装の早い段階から提供されている他、chromiumfirefoxにバックポートされたstd::span実装においては[]で境界チェックを行なっているため、std::spanに移行した際に対応する安全な関数が存在しないなど問題があります。

この提案は、一貫性と安全性の向上のために、std::spanにも.at()メンバ関数を追加しようとするものです。

namespace std {

  template<class ElementType, size_t Extent = dynamic_extent>
  class span {
    ...

    // 添字演算子(既にある
    constexpr reference operator[](size_type i) const;

    // at()関数(この提案
    constexpr reference at(size_type i) const;

    ...
  };
}

この提案の.at()メンバ関数は既存の他のコンテナと同様に、実行時に境界チェックを行った上で要素アクセスを行う関数であり、範囲外アクセス時にはstd::out_of_range例外をスローします。

P2824R0 WG21 February 2023 Issaquah meeting Record of Discussion

2023年2月6-11日にアメリカのIssaquahで行われた、WG21全体会議。

N4943との差異はよくわかりません。

P2825R0 calltarget(unevaluated-call-expression)

与えられた式が呼び出す関数の関数ポインタを取得する言語機能の提案。

関数がオーバーロードされている場合や関数テンプレートの場合、その関数名から関数ポインタを取得しようとするとうまくいかないことがあります。

#include <functional>

void f(int) {}
void f(double) {}

template<typename T>
void g(T) {}

int main() {
  std::function<void(int)> func1{f}; // ng
  std::function<void(int)> func2{g}; // ng
}

これは対象となる関数が1つに定まらないために起きています。

このような場合、取得するオーバーロードを確定させてから関数ポインタを取得する必要があります。

#include <functional>

void f(int) {}
void f(double) {}

template<typename T>
void g(T) {}

int main() {
  std::function<void(int)> func1{static_cast<void(*)(int)>(f)}; // ok
  std::function<void(int)> func2{g<int>}; // ok
}

このことはstd::functionだけではなく、呼び出し可能なものをテンプレートで受け取るようなところ(特に、型消去コールラッパのようなものを利用するところ)ではほぼ確実に発生します。回避策としては

  • static_castする
  • 一旦関数ポインタに受ける
  • 常に関数呼び出し(f())を使用する(汎用性を捨てる)
  • ラムダでラップする

などがあります。とはいえ、これらの解決策にはどれも様々な問題があります。

この提案は、このような関数ポインタ取得時のオーバーロードを考慮した対象関数の問い合わせを行う言語機能を追加することで、コンパイル時にコンパイラがこれを解決するようにしようとするものです。

この提案では、__builtin_calltarget(postfix-expression)という組み込み関数を言語機能として追加し、これに関数呼び出しの式を渡すことでその式で呼び出される関数のポインタを得られるようにします。ここでのpostfix-expressionは評価されないオペランドであり、実行されるわけではありません。

#include <functional>

void f(int) {}
void f(double) {}

template<typename T>
void g(T) {}

int main() {
  std::function<void(int)> func1{__builtin_calltarget(f(1))}; // ok
  std::function<void(int)> func2{__builtin_calltarget(g(1))}; // ok
}

__builtin_calltargetにはメンバ関数呼び出しを渡すこともできて、その場合はメンバ関数ポインタが得られます。渡された式のトップレベルのASTに関数呼び出しが含まれていない場合(式の一番最後に評価されるのが関数呼び出しでない場合)はコンパイルエラーとなります。

なお、__builtin_calltargetとは仮の名前であり、後で適切な名前に置き換えることを意図しているようです。

提案より、振る舞いの例

void g(long x) { return x+1; }
void f() {}                                                // #1
void f(int) {}                                             // #2
struct S {
  friend auto operator+(S, S) noexcept -> S { return {}; } // #3
  auto operator-(S) -> S { return {}; }                    // #4
  auto operator-(S, S) -> S { return {}; }                 // #5
  void f() {}                                              // #6
  void f(int) {}                                           // #7
  S() noexcept {}                                          // #8
  ~S() noexcept {}                                         // #9
  auto operator->(this auto&& self) const -> S*;           // #10
  auto operator[](this auto&& self, int i) -> int;         // #11
  static auto f(S) -> int;                                 // #12
  using fptr = void(*)(long);
  auto operator void(*)() const { return &g; }             // #13
  auto operator<=>(S const&) = default;                    // #14
};
S f(int, long) { return S{}; }                             // #15
struct U : S {}

void h() {
  S s;
  U u;
  __builtin_calltarget(f());                     // ok, &#1             (A)
  __builtin_calltarget(f(1));                    // ok, &#2             (B)
  __builtin_calltarget(f(std::declval<int>()));  // ok, &#2             (C)
  __builtin_calltarget(f((short)1));             // ok, &#2 (!)         (D)
  __builtin_calltarget(s + s);                   // ok, &#3             (E)
  __builtin_calltarget(-s);                      // ok, &#4             (F)
  __builtin_calltarget(-u);                      // ok, &#4 (!)         (G)
  __builtin_calltarget(s - s);                   // ok, &#5             (H)
  __builtin_calltarget(s.f());                   // ok, &#6             (I)
  __builtin_calltarget(u.f());                   // ok, &#6 (!)         (J)
  __builtin_calltarget(s.f(2));                  // ok, &#7             (K)
  __builtin_calltarget(s);                       // error, constructor  (L)
  __builtin_calltarget(s.S::~S());               // error, destructor   (M)
  __builtin_calltarget(s->f());                  // ok, &#6 (not &#10)  (N)
  __builtin_calltarget(s.S::operator->());       // ok, &#10            (O)
  __builtin_calltarget(s[1]);                    // ok, &#11            (P)
  __builtin_calltarget(S::f(S{}));               // ok, &#12            (Q)
  __builtin_calltarget(s.f(S{}));                // ok, &#12            (R)
  __builtin_calltarget(s(1l));                   // ok, &#13            (S)
  __builtin_calltarget(f(1, 2));                 // ok, &#15            (T)
  __builtin_calltarget(new (nullptr) S());       // error, not function (U)
  __builtin_calltarget(delete &s);               // error, not function (V)
  __builtin_calltarget(1 + 1);                   // error, built-in     (W)
  __builtin_calltarget([]{
       return __builtin_calltarget(f());
    }()());                                      // ok, &2              (X)
  __builtin_calltarget(S{} < S{});               // error, synthesized  (Y)
}

基本的には、組み込みの演算子やコンストラクタ/デストラクタなどはアドレスを取得することができないようになっています。GやJのケースは、定義されているクラス(UではなくS)におけるポインタを取得することを示しています。また、Nのケースは対象となる関数呼び出しの決定は構文定義によることを示しており、postfix-expressionsの構文は左結合であるためASTのトップに来るのは->ではなくf()となり、S::f(#6)のアドレスが取得されます。

P2826R0 Replacement functions

ある関数を指定したシグネチャでそのオーバーロード集合に追加できるようにする、一種の関数エイリアスの提案。

C++の様々なところで、何かラッパを挟むことなくある関数を別の名前あるいは呼び出しパターンの関数のオーバーロード集合に追加したいことがよくあります。標準ライブラリ自体にも、expression-equivalentとして指定されているところ(主にCPOの呼び出し)では、まさにそれが求められていますが、そのような方法はないのでexpression-equivalentという言葉で規定されています。

そのような場所でやりたいことは次のようなことです

  1. 関数の呼び出しパターンを検出する
  2. そのケースに適した関数を呼び出す

(CPOの定義に関して考えてみると実感が湧きやすいかもしれません)

問題となるのは、パターンの検出もその結果を受けての関数呼び出しも、どちらも多くの場合にC++オーバーロード解決メカニズムに適合しないことです。そのため、CPOのようなディスパッチを行うラッパが必要となります。

しかし、ラッパを使用すると、その内部でどのように工夫して実装したとしても入力のprvalueを他の右辺値と区別する方法がないため、コピー省略を妨げてしまいます。また、そのようなラッパはテンプレートの肥大化の原因ともなり、余分なコールスタックを追加することによってコピー省略以外の最適化を阻害したりデバッガーによるデバッグのしやすさを低下させます。

この提案は、expression-equivalentのようなユースケースをカバーし、なおかつ上記のような問題が起こらない、言語機能による関数エイリアスを提案するものです。

この提案による関数エイリアスは次のようなものです

// フリー関数の例

struct S {};
struct U {
  // Sへの暗黙変換演算子
  operator S() const { return{}; }
};

int g(S) { return 42; }

// g(S)をf(U)としてfのオーバーロード集合に入れる
auto f(U) = &g;

long f(S) { return 1l; }

int h() {
  return f(U{}); // returns 42
}
// メンバ関数の例

template <typename T>
struct Container {
  auto cbegin() const -> const_iterator;
  auto begin() -> iterator;

  auto begin() const = &cbegin; // saves on templates
};

関数宣言(定義)において、関数本体を= constant-expression;で定義し、右辺の式が定数式で何らかの関数ポインタに解決されれば左辺のシグネチャでその関数を呼び出すことができるようになります。ちょうど、関数のdelete宣言で= delete;、純粋仮想関数で= 0;と定義するように、左辺の関数名(とシグネチャ)を右辺の別の関数で定義します。

オーバーロード解決においては、左辺のシグネチャオーバーロード集合に参加し、オーバーロード解決でそれが選択されたらそれを右辺の関数で置換します。この時、置換後の関数がそのシグネチャで呼び出し可能ではない場合、そのままエラーになるだけです。

この提案による関数宣言の右辺の定数式がnull(ptr)に評価された場合、その関数はdelete定義されたものとみなされます。これによって、C++の式として関数のdeleteを制御できるようになります。ただしこれは、仮想関数の場合は= 0;で定義されたものとみなされます。

template <bool enable_implementations>
struct A {
  long g(int) { return 42; }

  long h(int) = enable_implementations ? &g : nullptr;
  
  virtual long f(int) = enable_implementations ? &g : nullptr;
};

struct Concrete : A<true> {};
struct Abstract : A<false> {};
struct Concrete2 : Abstract { 
  long f(int) override { return 3; }
};

void impl() {
  Concrete x;  // ok
  x.h(2);      // ok, 42

  Concrete2 z; // ok
  z.f(2);      // ok, 3、f(int)はConcrete2でオーバーライドされている
  z.h(2);      // Error、h()は削除されている

  Abstract y;  // Error、f(int)は純粋仮想関数であり、Abstractは抽象クラス
};

この提案によって解決される言語の問題には例えば、P2481で報告されている、型のCV/参照修飾だけを推論したい場合の構文が無い問題があります。

struct A {};
struct B : A {};

// 関数f()の実装
template <typename T>
auto _f_impl(T&& x) {
  // A&, A&&, A const&, A const&& に対してのみインスタンス化される
};

// cvref-derived-fromのようなコンセプト
template <typename D, typename B>
concept derived_from_xcv = std::derived_from<std::remove_cvref_t<D>, B>;

// Aの派生型TをAにアップキャストしつつそのCV修飾をコピーして、_f_impl()に渡すエイリアス
template <derived_from_xcv<A> T>
void f(T&&) = &_f_impl<copy_cvref_t<T, A>>;

void use_free() {
  B b;
  f(b);                // OK, calls _f_impl(A&)
  f(std::move(b));     // OK, calls _f_impl(A&&)
  f(std::as_const(b)); // OK, calls _f_impl(A const&)
}

同じことは明示的オブジェクトパラメータを持つ関数でも問題になる可能性があり、この提案による関数定義はそれもサポートしています。

struct C {
  // 実装関数、Cだけを受け取る
  void f(this std::same_as<C> auto&& x) {
    ...
  }

  // Cの派生型TをCにアップキャストしてf()に渡すエイリアス
  template <typename T>
  void f(this T&& x) = static_cast<void (*)(copy_cvref_t<T, C>&&)>(f);

  // 前項のP2825の __builtin_calltarget()を使用すると、対象関数の選定をコンパイラに委託できる
  template <typename T>
  void f(this T&& x) = __builtin_calltarget(std::declval<copy_cvref_t<T, C>&&>().f());
};

struct D : C {};

void use_member() {
  D d;
  d.f();                // OK, calls C::f(C&)
  std::move(d).f();     // OK, calls C::f(C&&)
  std::as_const(d).f(); // OK, calls C::f(C const&)
}

P2827R0 Floating-point overflow and underflow in from_chars (LWG 3081)

std::from_chars浮動小数点数を変換する際にアンダーフローを検出できるようにする提案。

std::from_charsは文字列から数値への変換を行う関数であり、変換後の値は引数に渡された数値型オブジェクトの参照へ出力します。変換後の数値型はその引数型から決まり、変換後の値がその型で表現できない場合は、戻り値のfrom_chars_result::ecstd::errc::result_out_of_rangeになると規定されています。

ただ、これは整数型におけるオーバーフローが起きた場合に合わせた仕様であり、浮動小数点数型におけるアンダーフローを判別できなくなっています。すなわち、std::errc::result_out_of_rangeが帰ってきた時に、変換後の値が大きすぎてdouble値に出力できなかったのか、変換後の値(の絶対値)が小さすぎてdouble値に出力できなかったのかが判別できません。これによって例えば、Pythonインタプリタが行う次のような振る舞いをstd::from_charsでは実現できないことになります

>>> 3.14e-2000
0.0
>>> -1.1e360
-inf

std::from_charsの規定はCのstrtod()を参照する形で規定されていますが、その移行時にstrtod()のこの種の機能性が失われていることがLWG Issue 3081で指摘されていました。

strtod()double型への変換の際に、オーバーフローする場合はHUGE_VALを返してerrnoERANGEをセットし、アンダーフローする場合は最小の正の正規化数を返しerrnoERANGEをセットするかは実装定義と規定されています。これだけみるとstd::from_charsとほぼ変わらないように見えますが、実際にはstrtod()の適切な実装としては、アンダーフローが起きた場合は0.0/-0.0を返してerrnoERANGEをセットするのがデファクトスタンダードとなっています。

すなわち、strtod()では、errnoERANGEである場合に結果が0であるかを確認することで、オーバーフローとアンダーフローを区別できます。

// C標準の適合実装に準じたコード
{
  errno = 0;
  double n = strtod(p, NULL);
  if (errno == ERANGE && (n == HUGE_VAL || n == -HUGE_VAL)) {
    // オーバーフロー検出
  }
}

// 実際のデファクト実装に準じたコード
{
  errno = 0;
  double n = strtod(p, NULL);
  if (errno == ERANGE) {
    if (n != 0.0) {
      // オーバーフロー検出
    } else {
      // アンダーフロー検出
    }
  }
}

浮動小数点数値(IEEE 754準拠)の比較では0.0-0.0は同値であると比較されるため、どちらが返されていたとしても0.0との比較がtrueを返すかによってアンダーフローを識別できます。

std::from_charsでこの問題を解決するために取れる方法は限られており

  • 例外を投げる
    • <charconv>の目的にそぐわない
  • グローバル状態の変更
    • 同上
  • std::from_chars_result::ecに報告する値の変更
    • 後方互換性を損ねる
    • 現在オーバーフロー/アンダーフローを区別する必要がないコードで、アンダーフロー時の動作が変わってしまう可能性がある
  • 出力値に特別な値を出力

実質的には最後の方法、結果を出力する変数に特別な値を出力すること、に限られています。

この提案では、その値としてアンダーフロー時には±0.0、オーバーフロー時には±1.0を出力するように変更することを規定しています。なお、現在は戻り値のエラーコードでresult_out_of_rangeを返す以外のことはしていません(変換失敗には出力先変数は変更されない)。

出力値の符号は変換後の値の符号から決まります。すなわち、vを変換後(出力前)の値とすると

  • +0.0 : vが正の値であり、アンダーフローが起きた時
  • -0.0 : vが負の値であり、アンダーフローが起きた時
  • +1.0 : vが正の値であり、オーバーフローが起きた時
  • -1.0 : vが負の値であり、オーバーフローが起きた時
// この提案によるオーバーフロー/アンダーフロー検知コード

double v;
if (auto [ptr, ec] = std::from_chars(first, last, v); ec == std::errc::result_out_of_range) {
  if (v != 0.0) {
    // オーバーフロー検出
  } else {
    // アンダーフロー検出
  }
}

ただ、std::from_charsの規定をよく読むと非有限値を出力することがないことがわかるため、それを利用した次のようなエラーハンドリングコードが考えられます

// 先にNaN(非有限値)で初期化
auto v = quiet_NaN_v<double>;

std::from_chars(first, last, v);

// 有限値ではない場合はエラーを仮定できる
if (not std::isfinite(v)) {
  /* ec != errc() */
}

このようなコードは稀であると思われるため、機能テストマクロの数値をバンプすることでユーザー側で対処してもらうことを提案しています。

P2828R0 Copy elision for direct-initialization with a conversion function (Core issue 2327)

CWG Issue 2327では、次のようなコードにおいてコピー省略が起きないことが報告されていました。

struct Cat {};
struct Dog { operator Cat(); };

Dog d;
Cat c(d); // Catのムーブコンストラクタでprvalueが実体化する

Dog::operatorCat()がコピー省略可能なように実装されていたとして、このコードでは、Cat cの初期化においてCatのムーブコンストラクタが選択され、dを変換演算子によってCatに変換した結果が渡されます。ムーブコンストラクタはCat&&でそれを受けるため、ここでprvalueの実体化が発生し、これによってコピー省略の要件を満たさなくなるためCatのムーブコンストラクタの呼び出しを削除できなくなります(Catのムーブコンストラクタがdeleteされているとコンパイルエラーとなる)。

ただ、少なくとも現時点のClang/GCC/MSVC/NVC++(EDG)はどれもこの場合のコピー省略を実装しており、Catのムーブコンストラクタがdeleteされていても上記コードはコンパイルエラーとなりません。

ただし、その実装のアプローチにはいくつかの違いがあるようで、それを調べていくとこの場合の(変換演算子を用いた直接初期化における)コピー省略の設計空間がかなり広いことが分かったようです。この提案は、CWG2327及び将来のコピー省略の拡大のために、その報告とそれらのアプローチの比較を行うものです。

その実装の戦略の違いは次のようになります(EDG/MSVCはソースコードがオープンではないので推測を含んでいます)

  • EDG
    • Cat初期化時の)オーバーロード解決は標準に従って行う
    • 直接初期化を行うためにCatのムーブ(orコピー)コンストラクタが選択され、コンストラクタの引数(Cat&&型)が変換演算子によって変換された結果のprvalueを束縛するためにそのprvalueが実体化する場合、一時オブジェクトを実体化させる代わりにそのprvalueを使って初期化対象のオブジェクトを初期化する
  • Clang
    • オーバーロード解決の時に、Catのムーブ(orコピー)コンストラクタに加えてDog::operator Cat()も直接初期化の候補として加える
    • オーバーロード解決では、Catのムーブ(orコピー)コンストラクタがDogの変換演算子呼び出しを必要とするのに対して、Dog::operator Cat()dをその暗黙のオブジェクトパラメータ(thisパラメータ)に束縛するのみであるため、Dog::operator Cat()が選択される
    • その結果、Dog::operator Cat()の呼び出しのみでd -> cの変換とcの初期化が完了し、ムーブコンストラクタは呼び出されない
  • GCC
    • オーバーロード解決は標準に従って行う
    • オーバーロード解決の前に、その候補となっているムーブ(orコピー)コンストラクタの参照パラメータ(Cat&&)が変換演算子の結果のprvalueを束縛する場合、そのコンストラクタを変換演算子そのもので置き換える
    • Clangの場合と同様に、変換演算子の優先度は残りうる他の物(変換コンストラクタなど)より高くなるため、(ムーブ(orコピー)コンストラクタを置き換えた)変換演算子が選択される
    • その結果、Dog::operator Cat()の呼び出しのみでd -> cの変換とcの初期化が完了し、ムーブコンストラクタは呼び出されない
  • MSVC
    • EDGに近い実装と思われるが若干異なる振る舞いをする。明確なルールを見出すことができなかった

どの実装の場合でもコピー省略が起こるのは、変換演算子の結果はCatそのもの(CV修飾は考慮される)でなければなく、その派生クラスや参照であってはいけません。すなわち、変換演算子cv Cat型のprvalueを返さないとコピー省略されません。

比較すると、EDGは最も保守的かつシンプルなアプローチをとっており、Clangはそれと対照的に最もアグレッシブなアプローチをとっています。GCCは両者の中間的な実装となり、MSVCはEDGに近い保守的なアプローチと思われます。

このアプローチの差は、当然その振る舞いにも差をもたらします。特に、EDGのアプローチは基本的に後方互換性を保っていますが、Clang(GCC)のアプローチはそうではない場合があります。

struct X {
  X(int);
  // X(X&&);  // implicitly declared
};

struct Y {
  operator X();
  operator int();
};

X x(Y{}); // clang/gccは曖昧にならない

この例では、現在のC++およびEDGではXのコンストラクタが曖昧となるためエラーとなります。一方、Clang/GCCは曖昧とならず、Y::operator X()によって初期化されます。これは、Clangの場合はY::operator X()オーバーロード解決候補に入れられ、変換が最小であるためそれが選択されており、GCCの場合はX::X(&&)Y::operator X()に置き換えられてオーバーロード解決によって(Clangと同様の理由で)選択されるためです。

CWGのコンセンサスは、Clang(GCC)のアプローチを好み、この例はコンパイルされるべき、という方向で合意されているようです。

struct Dog;

struct Cat {
  Cat(const Dog&);
};

struct Dog {
  operator Cat();
};

Cat cat(Dog{}); // EDG/MSVCは変換コンストラクタを呼ぶ
                // Clang/GCCは変換演算子を呼ぶ

この例では、Dog -> Catの変換にCatの変換コンストラクタとDogの型変換演算子のどちらが使われるのかが実装によって異なります。EDG/MSVCはCatの変換コンストラクタを選択し呼び出しますが、Clang/GCCDogの変換演算子を選択し呼び出します。これは、Clang/GCCがどちらもDog::operator Cat()Cat::Cat(const Dog&)の間でオーバーロード解決を行うためで、型変換なしのconst参照への束縛よりもthisパラメータへの束縛の方が優先順位が高くなるため変換演算子が選択されます。

また、この場合にDog::operator Cat()が次のように実装されていると、Clang/GCCでは無限ループに陥ります。

Dog::operator Cat() {
  return Cat(*this);  // Clang/GCCは無限再帰する・・・
}

この例のClang/GCCの振る舞いは既存のコードの振る舞いを静かに変化させています。CWGのコンセンサスはこの例の動作を変更させないことで合意されているようです(つまり、GCC/Clangのアプローチはそのまま標準化できません)。

struct T {
  T(T const&);
};

struct S {
  operator T();
  operator T&();
};

S s;
T t(s); // Clangのみ、operator T()を呼び出す

この例では、現在のC++及びEDG/MSVC/GCCS::operatorT&()を呼び出します。これは、SからT const&への変換において一時オブジェクトを生成するよりも参照を束縛する事の方が優先されるためです。しかし、Clangはオーバーロード候補にS::operator T()Tのコンストラクタを追加し、結果としてS::operator T()が選択されます。S::operatorT&()は使用されません。

struct Y;

struct X {
  X(const Y&);
};

struct A {
  operator X();
};

struct B {
  operator X();
};

struct Y : A, B { };

X x(Y{});  // Clangは曖昧になる

この例は、Clangのアプローチだけが曖昧となるように考えられた例であり、現実的なコードでは見られないものかもしれません。オーバーロードの解決候補に、Xのコンストラクタに加えてA, Bに定義された2つのoperator X()が加えられ、それらの間で順序が付かない事から曖昧になります。この場合、X::X(Y&&)を追加すると曖昧さを取り除くことができます。

このように、Clang(およびGCC)のアプローチで行われているオーバーロード解決ルールに変更を加えることは、現行のC++及びEDGの保守的なアプローチと比較して既存のコードに問題を起こすことがあります。CWGの議論では、Clangのアプローチが当初は好まれていましたが、上記例のようないくつかの問題が明らかになった結果として、現在はどのアプローチを採用するかのコンセンサスがなくなっています。

そのためこの提案では、CWG Issue 2327の解決としては保守的なEDGのアプローチを採用し、アグレッシブなコピー省略のためのオーバーロード解決ルールの変更は別の機会に回すことを提案しています。EDGの保守的なアプローチを取ったとしても、将来的にClangの様なアプローチを採用することを妨げないため、CWG Issue 2327の問題はとりあえず解決しつつより時間をかけて議論したうえでさらなるコピー省略保証を導入することができます。

P2830R0 constexpr type comparison

std::type_info::before()constexprにする提案。

std::type_info::before()は実装定義ではあるものの、2つの型の間に順序を付ける関数です。これは本来コンパイル時のプロパティであり、C++20-23でstd::type_infoの取得と==による比較がconstexpr対応しているにも関わらず、この関数はconstexprではなく定数式で使用できません。

複数のポリシー型を受け取るテンプレートなどにおいてそのテンプレートパラメータを一定の順序でソートすることができれば、異なる翻訳単位の間で同じエンティティに対して同じシンボルを提供できるようになるなど、コンパイル時に型のソートを行うことは有用である可能性があります。ソートを行うためには<による比較が必要であり、しかもそれが移植可能であるためには実装の間で型の順序が一貫している必要があります。

この提案では、std::type_info::before()constexprにするために、C++における全ての型の順序付けがどのように決定されるかを詳細に検討するものです。この提案の内容はまた、std::meta::infoに全順序比較を提供するための順序付けのサブセットでもあるため、リフレクションの設計にも影響を与える可能性があります。

この提案のアプローチは次の手順によって順序付けを行います

  1. 言語内のすべての型について、key-tupleへの変換を定義する
  2. このkey-tupleによって順序を定義する

key-tupleは次の2つのいずれかを要素とするタプルです

key-tuple内の要素はatom優先の辞書式比較によってソートされ、2つのkey-tupleの比較は短い方に合わせて詰められます。

あるエンティティに対してこのkey-tupleを求める操作をsort_key(entity)とすると、例えば次のような型は

namespace foo::bar {
  struct i;
}

namespace baz {
  struct j;
}

それぞれ次のようなkey-tupleに変換されます

sort_key(foo::bar::i) = ((namespace, foo), (namespace, bar), (type, i))
sort_key(baz::j)      = ((namespace, baz), (type, j))

このとき、名前空間baz < fooであるため、baz::j < foo::bar::iのように順序付けされます。

atomは次のいずれかのものです

  1. kinds
    • トークンの種別を示す、次のいずれか
      1. value
      2. namespace
      3. type
      4. class template
      5. type alias template
      6. variable template
      7. concept
      8. function
  2. simple names
    • atom要素の名前を示す文字列
  3. qualifiers
    • 修飾子、次のいずれか
      • &
      • &&
      • const
      • volatile
  4. [] (要素数不明の配列型)
  5. [n] (要素数既知の配列型)
  6. * (ポインタ型)
  7. ... (ellipsis)
  8. パラメータパック (ellipsisと区別される)

atomはこの順番で順序付けされます。

名前空間namespace-nameに対するsort_key(namespace-name)(namespace, namespace-name)のようなkey-tupleに変換されます。これは、対応する位置で名前空間名がアルファベット順で並べられることを反映しています。

namespace outer1 {
  struct i;
}

namespace outer2 {
  namespace inner1 {
    struct i;
  }
  namespace inner2 {
    struct i;
  }
}

この3つの構造体は、sort_key(outer1::i) < sort_key(outer2::inner1::i) < sort_key(outer2::inner2::i)のように並べられます。

typeに対するsort_key(type)(type, <simple names>, <qualifiers>)のようなkey-tupleに変換されます。

qualifiersは次のようなスコアをすべて加算して、その結果によって昇順で並べられます。

&: 1
&&: 2
const: 3
volatile: 6

修飾なしの型をTとすると、可能な修飾子による順序は次のようになります

0  T
1  T &
2  T &&
3  T const
4  T const &
5  T const &&
6  T volatile
7  T volatile &
8  T volatile &&
9  T const volatile
10 T const volatile &
11 T const volatile &&

列挙型を除いた組み込みのスカラ型は、複合型よりも前に順序付けられます。スカラ型間の順序は次のようになります

  1. void
  2. std::nullptr_t
  3. bool
  4. char, signed char, unsigned char
  5. 整数型
    • ビット幅で順序付けした後で、符号付 < 符号なし で順序付け
  6. 上記のいずれかの型エイリアスではない、残りの文字型
  7. 浮動小数点数
    • float < double < long doubleの後に、そのほかの浮動小数点数型をサイズ順で順序付け
  8. 関数型
    • 戻り値型 -> 引数型 の辞書式順序によって順序付け
  9. ポインタ型
    • 参照先の型(*を取った型)によって順序付け
  10. メンバポインタ型
    • 参照先の型(*を取った型)によって順序付け
  11. 配列型
  12. クラス型

関数型のkey-tupleは、sort_key(<function>) = (function, <name>, sort_key(<return type>), (sort_key(<parameter>)...))のように変換されます。例えば

sort_key(void foo(int i)) = (function, foo, (type, void), ((type, int)))
sort_key(void foo(int)) = (function, foo, (type, void), ((type, int)))
sort_key(void foo(int, double)) = (function, foo, (type, void), ((type, int), (type, double)))

配列型は、sort_key(T[]) = ([], sort_key(T))もしくはsort_key(T[n]) = ([n], sort_key(T))のように変換され、要素の型->要素数の辞書式順序で順序付けされ、要素数不明の配列型は要素数既知の配列型の前に順序付けされます。

T[]
T[10]
T[11]
T[][2]
T[10][2]
T[3][2]

は、T[] < T[10] < T[11] < T[][2] < T[3][2] < T[10][2]のように並べられます。

非テンプレートのクラス型は、クラス名のアルファベット順で順序付けされます。

struct Apple {};
class Banana {};
struct Carrot {};

は、Apple < Banana < Carrotの順で並べられます。

クラステンプレートの場合は、sort_key(<class template>) = (type, (<name>, (sort_key(<parameter>)...)))のように変換され、クラス名 -> テンプレートパラメータの辞書式順序、によって順序付けされます。

template <typename T, typename U>
struct Apple;

struct Banana;
struct Carrot;

Apple<Banana, Carrot>;
Apple<Banana, Banana>;
Apple<Carrot, Carrot>;

に対して

sort_key(Apple<Banana, Carrot> = (type, (Apple, (sort_key(Banana), sort_key(Carrot)), )
                               = (type, (Apple, ((type, Banana, ), (type, Carrot, )), )

のようになり、Apple<Banana, Banana> < Apple<Banana, Carrot> < Apple<Carrot, Carrot>のように並べられます。

他にも、NTTP、メンバ関数型、可変長関数型、パラメータパック、変数テンプレート、エイリアステンプレート、コンセプト、等の順序付けについて検討されています。いずれも、エンティティ名としてアルファベット以外を考慮していませんが、その範囲内では直感的な順序付けになっています。

P2833R0 Freestanding Library: inout expected span

C++23のライブラリ機能の一部をFreestanding指定する提案。

この提案のFreestanding化候補は次のものです

また、std::out_ptr/std::inout_ptrファミリはstd::shared_ptrに対して使用することができ、std::shared_ptrは現状フリースタンディング指定されていないため、reestanding-deletedと指定することも提案されています。この指定がなされたクラステンプレートが実装(定義)されるか未定義であるかは実装の自由とされ、これによって、std::shared_ptrの実装が可能なライブラリ実装ではstd::shared_ptrをフリースタンディング環境で提供することができ、その実装が不可能な環境では未定義とすることで使用された場合にコンパイルエラーを発することができます。

P2836R0 std::const_iterator often produces an unexpected type

std::const_iterator<I>とコンテナ型のconst_iteratorの不一致を正す提案。

C++23で追加されたstd::const_iterator<I>は、イテレータIの間接参照結果をas_constして返すようなイテレータラッパです。これは、Iの参照型が既にconstである場合を除いてstd::basic_const_iterator<I>を使用し、std::basic_const_iteratorは単にIをラッパするクラス型です。

これによって例えば、std::const_iterator<int*>int const*ではなく、std::basic_const_iterator<int*>になります。これ自体も期待と一致しないかもしれませんが、さらに、コンテナ型からの定数イテレータの取得方法によって取得されるイテレータに差が生じる場合があります。

using V = std::vector<int>;
auto v = V();

using I1 = decltype(ranges::cbegin(v));
using I2 = ranges::const_iterator_t<V>;

static_assert(std::same_as<I1, I2>);  // fail!!

I1V::const_iteratorであるのに対し、I2std::basic_const_iterator<V::iterator>となり、このstatic_assertは失敗します。

ranges::cbegin(r)は、rangeのオブジェクトrに対してas_const(r).begin()を呼ぶのとほぼ同等の事を行い、標準コンテナの場合これはそのconst_iteratorを返すため、I1V::const_iteratorになります。

ranges::const_iterator_t<R>const_iterator<iterator_t<R>>に置換され、RイテレータIの読み取りがすでにconstでない場合はstd::basic_const_iterator<I>になり、標準コンテナをそのまま渡す場合はstd::basic_const_iterator<R::iterator>になります。

このように、定数イテレータを取得する経路の違いによって、このような差が生じています。

あるイテレータに対する定数イテレータの決定はこのようにその文脈によって変化し、振る舞いとしては一致しているかもいせませんが、型としての一貫性がありません。ranges::iterator_t<const R>ranges::const_iterator_t<R>は類似している必要があると思われ、この差異はその期待を破っています。これは、C++のライトユーザーにとっても専門家にとっても、期待通りの事ではないはずです。

この提案は、この問題を解決するために、まずconst_iterator_forを導入して

namespace std {
  // プライマリテンプレート
  template<input_iterator>
  struct const_iterator_for {};

  // const修飾を外すための部分的特殊化
  template<input_iterator I>
  struct const_iterator_for<const I> : const_iterator_for<I> {};
  
  // ポインタ型に対する部分的特殊化
  template<input_iterator I>
    requires is_pointer_v<I>
  struct const_iterator_for<I> {
    // T* -> T const * へ変換
    using type = const remove_pointer_t<T>*;
  };

  // 入れ子型const_iterator_forを持っている型に対する部分的特殊化
  template<input_iterator I>
    requires requires { typename I::const_iterator_for; }
  struct const_iterator_for<I> {
    using type = typename I::const_iterator_for;
  };

  // const_iterator_forを得るためのエイリアステンプレート
  template<input_iterator I>
    requires requires { typename const_iterator_for<I>::type; }
  using const_iterator_for_t = typename const_iterator_for<I>::type;
}

次に、標準ライブラリの全てのコンテナ型のイテレータ型のメンバ型としてconst_iterator_forを追加し、それを対応するconst_iteratorへリンク(エイリアス)することで、const_iterator_for_tを介してイテレータIから対応する定数イテレータを取得できるようにします。

そして、これを用いて次の2つの解決策を提案しています

  1. エイリアステンプレートstd::const_iteratorの定義を修正する
    • const_iterator_for_t<I>が利用できない場合にのみ、basic_const_iteratorを使用する
  2. std::basic_const_iteratorconst_iterator_for_t<I>への暗黙変換演算子を追加する

この提案は1の解決策を推しており、それはMSVC/STLをフォークして実装され、MSVC/STL実装者のレビューを受けているとのことです。

ただし、1の変更は元のconst_iteratorの意図的な設計上の選択を変更することになるため、ABIを破損します。とはいえ、現時点でMSVCのプレビュー版でしか実装されておらず、MSVCがそれを含んだABI安定版をリリースしていないため、今ならまだそれは間に合います。

P2838R0 Unconditional contract violation handling of any kind is a serious problem

契約プログラミング機能において、違反時の振る舞いを規定するモードをハードコードするのではなく、違反ハンドラをカスタマイズすることをデフォルトとする提案。

現在進行中のC++契約プログラミング機能においては、契約違反が発生した場合の振る舞いをビルドモードを通してカスタムするようにしており、現在はEval_and_abortモードのみがサポートされています。ここに、Eval_and_throwモードを追加しようとする提案(P2698)もあり、おそらく将来的には違反後にそれを無視して継続するようなモードも想定されます。

いずれにしても、これらの契約違反時の振る舞いはビルドモード(つまり、コンパイラオプション)によって変更されるものであり、契約違反時に異なる振る舞いをさせたくなった場合はプログラム全体の再コンパイルが必要となります。これはユーザーが直接書いているコードだけでなく、依存しているライブラリについても全て再コンパイルが必要です。どう考えてもそれは重すぎるコストであり、必ずしも常に可能であるわけではありません。それに対応するために、ビルド済みライブラリを提供するベンダー(あるいはOSS)は、契約機能のビルドモード毎にビルド済みバイナリを提供することになるかもしれません。これにもコストがかかります。

もしもこれを解消できこのようなコストを回避できる方法があるならば、それを最初から採用しておくべきです。

このことは、C++26(予定)以降の漸進的な修正・変更によって解決できる技術的な問題ではなく、契約機能をどのように使用できるのか、それを使用するコードをどのようにパッケージ化して配置可能なのか、どのような場合にバイナリの再コンパイルが必要となり、複数のバイナリを事前に提供しておく必要があるのか、まだどのような場合にその必要がないのかに直接影響します。すなわちこれは、ライブラリベンダーとそのユーザーの双方に直接影響を与え、OSSであるかに関係なくビルド済みバイナリの出荷方法に影響を与えます。

この提案によるこれら問題の解決策は、P2811を最初のC++契約機能に組み込むことです。

(P2811は上の方で解説済みなので詳しくはそちらを参照)

P2811では、契約違反時に呼ばれる関数である違反ハンドラをユーザーがカスタマイズ可能とすることを提案しています。これによって、契約違反時の振る舞いは違反ハンドラ(とそのカスタマイズ)を通して実装可能になります。

例えば、Eval_and_abortモードはデフォルトの振る舞いであり、違反ハンドラがその処理を終えて正常にリターンした場合にプログラムを中止することで実装されます。Eval_and_throwモードは、違反ハンドラをカスタマイズ可能(ユーザーが置換可能)とすることで、ユーザー定義の違反ハンドラ内から任意の例外をスローすることで実装可能となります。他にも、呼び出されるとそのスレッドをスリープさせたり、スピンループに入るように違反ハンドラをカスタムすると、あるスレッドで契約違反が起きた時でもプログラム全体を停止させずに続行させる動作を実装することができます。

現在のMVPの仕様の下でも、後からこのような違反ハンドラを通した実装に変更することは純粋な仕様の上では可能です。ただし、現在のMVPは実装がそのQoIの範疇で違反ハンドラの実装を省略(同等の振る舞いをするコードを翻訳単位にハードコーディング)することが可能であり、この選択をした実装が現れてしまえば、後からこの選択を取り消すことはできなくなります。その場合結局、最初の問題から逃れることができなくなります。

おわり

次回の提案公開時期は5月の終わりごろとのことなので、次回の投稿は6月になりそうです。

この記事のMarkdownソース

[C++]WG21月次提案文書を眺める(2023年02月)

文書の一覧

SG22のWG14からのものを除いて、全部で102本あります。

P0290R3 apply() for synchronized_value

P0290R4 apply() for synchronized_value

ミューテックスを用いた値への同期アクセスをラップするユーティリティの提案。

この提案のsynchronized_value<T>Tの値とミューテックスをペアで保持する型で、保持するTの値へのアクセスを保持するミューテックスによって同期化するものです。

// synchronized_valueの宣言例
namespace std::experimental::inline concurrency_v2 {

  template<class T>
  class synchronized_value {
  public:
      synchronized_value(synchronized_value const&) = delete;
      synchronized_value& operator=(synchronized_value const&) = delete;

      template<class ... Args>
      synchronized_value(Args&& ... args);

  private:
      T value;   // exposition only
      mutex mut; // exposition only
  };

  template<class T>
  synchronized_value(T) -> synchronized_value<T>;
}

読み出しと書き込みを直接サポートしていませんが、それがあったとしてもそれだけならstd::atomicで十分であり、存在理由がありません。

ミューテックスを用いた値アクセスの同期化がアトミックアクセスと異なるところは、ミューテックスのロックと解放によって1度のアクセスを超えた範囲のクリティカルセクションを確保できることにあります。synchronized_value<T>はそのためにapply()メンバ関数を提供します。

// apply()の宣言例
namespace std::experimental::inline concurrency_v2 {

  template<class F,class ... ValueTypes>
  invoke_result_t<F, ValueTypes&...> apply(F&& f, synchronized_value<ValueTypes>&... values);
}

apply()は、1つ以上のsynchronized_value<T>とそれと同じ数のT...の値から呼び出し可能なfを受けて、f(T...)の呼び出し前後でvaluesの全てのミューテックスのロックと解放を自動でかつ適切に行うことでT...の値に対するクリティカルセクション内でfを実行します。

synchronized_value<std::string> s;

// 単純な読み出しの例
std::string read_value() {
  // apply()に渡した関数はsのmutexによるクリティカルセクション内で実行される
  return apply([](auto& x){ return x; }, s);
}

// 単純な書き込みの例
void set_value(const std::string& new_val) {
  // apply()に渡した関数はsのmutexによるクリティカルセクション内で実行される
  apply([&](auto& x){x=new_val;}, s);
}

synchronized_value<T>の保持する値へのアクセスはこのapply()を通してのみ行うことができ、使用間違いを防ぐために構築以外の操作は提供されていません。

ミューテックスを用いたアクセスの同期化においては同期対象の値とミューテックスオブジェクトがセットで扱われることになることが多いですが、コード上での記述はどうしても複数の変数宣言に分かれてしまうためセットは意味的なものとしてしか表現できません。また、実際のクリティカルセクションの作成においても、std::lock_guardなどである程度自動化できるとはいえ、少なくともロックは手動で行う必要があり、その際に使用するミューテックスも明示的に指定しなければなりません。

synchronized_value<T>apply()を用いると、同期対象の値とそのためのミューテックスのペアを型によって表現することができ、クリティカルセクションの作成においても手動でミューテックスを触る必要がなくなります。これによって、コードの可読性向上や記述ミスの防止などを図ることができます。

提案文書よりサンプルコード

より複雑な処理の例

// 何かメッセージのキューを同期化する
synchronized_value<std::queue<message_type>> queue;

void process_message(){
  std::optional<message_type> local_message;

  // グローバルなキューからメッセージを1つ読み出してくる
  apply([&](std::queue<message_type>& q) {
      if(!q.empty()) {
        // 先頭メッセージ取り出し(クリティカルセクション)
        local_message.emplace(std::move(q.front()));
        q.pop_front();
      }
  }, queue);
  
  // 読み出しに成功していたら、それを使って何かする
  if(local_message) {
    do_processing(local_message.value());
  }
}

複数の値を処理する例

// 口座間でお金を転送する例
void transfer_money(synchronized_value<account>& from_, // 転送元
                    synchronized_value<account>& to_,   // 転送先
                    money_value amount)                 // お金オブジェクト
{
  apply([=](auto& from, auto& to) {
    // 引き出して
    from.withdraw(amount);
    // 預け入れ
    to.deposit(amount);
  }, from_, to_);
}

このような複数のsynchronized_value<T>に対する操作では特に、複数のミューテックスを用いたアクセスにおけるデッドロックを回避できるというメリットもあります。

このように、複数のsynchronized_value<T>に対して何か関数を適用するという形は、std::tupleに対するstd::apply()とよく似たものなので、名前もそこから取っています。

この提案はConcurrency TS v2向けに提案されており、2月のIssaquah会議でConcurrency TS v2に採択されています。

P0447R21 Introduction of std::hive to the standard library

要素が削除されない限りそのメモリ位置が安定なコンテナであるstd::hive(旧名std::colony)の提案。

以前の記事を参照

このリビジョンでの変更は、ブロック容量の制限がstd::hiveオブジェクト間でコピーされる条件についてDesign Decisionsセクションに追記し提案する文言に正式に記載した、Appendix Fの修正、Design Decisionsセクションのタイトルを修正、などです。

P0493R4 Atomic maximum/minimum

std::atomicに対して、指定した値と現在の値の大小関係によって値を書き換えるmaximum/minimum操作であるfetch_max()/fetch_min()を追加する提案。

以前の記事を参照

このリビジョンでの変更は

  • 使用していなかったベンチマークの削除
  • メンバ関数についてフリースタンディングであることを明記
  • fetch_max(), fetch_min()remarkを追加
  • ポインタの比較についてnoteを追加
  • ポインタの操作について説明を追記

などです。

この提案はC++26をターゲットして、LWGによるレビューを終えています。次の全体会議で投票にかけられる予定です。

P0792R13 function_ref: a non-owning reference to a Callable

P0792R14 function_ref: a non-owning reference to a Callable

Callableを所有しないstd::functionであるstd::function_refの提案。

以前の記事を参照

このリビジョンおよびR13での変更は、LWGのフィードバックによる文言の調整と、フリースタンディング指定の修正などです。

この提案はすでにLWGのレビューをパスして、次の全体会議にかけられることが決まっています(C++26ターゲットです)。

P0870R5 A proposal for a type trait to detect narrowing conversions

Tが別の型Uへ縮小変換(narrowing conversion)を起こさずに変換可能かを調べるメタ関数is_convertible_without_narrowing<T, U>を追加する提案。

以前の記事を参照

このリビジョンでの変更は、LWGのフィードバックの反映、変換元が定数式であることを考慮しないという意図的な選択についての解説を追記したことなどです。

この提案はLEWGのレビューをパスしたLWGに転送されています。

P0876R12 fiber_context - fibers without scheduler

スタックフルコルーチンのためのコンテキストスイッチを担うクラス、fiber_contextの提案。

以前の記事を参照

このリビジョンでの変更は、fiber_contextからstop_tokenサポートを取り除いたこと、呼び出し側の提供する未初期化メモリ領域をファイバーのコールスタックとして使用するためのコンストラクタを追加したことです。

stop_tokenサポートが取り除かれたのはそれについて実装の懸念が生じたためのようです。各ファイバー(そのコールスタック)自体はfiber_contextの寿命とは無関係な永続的なエンティティですが、fiber_contextはそうではありません。fiber_contextの新しいオブジェクトは常に中断状態で生成され、これによってファイバーを一時停止するコードは関連するstop_source共有状態を見つけられなくなります。

stop_tokenを使用したいユーザーは、自身でstop_sourceを管理した上でそこから取得したstop_tokenfiber_contextに渡すラムダ式に渡しておけばよく、fiber_contextで直接サポートする必要はない、とのことです。

// fiber_contextの宣言例
namespace std::experimental::inline concurrency_v2 {
  class fiber_context {
  public:
    fiber_context() noexcept;

    template<typename F>
    explicit fiber_context(F&& entry);

    // コールスタック配置に使用するメモリ領域を受け取るコンストラクタ
    template<typename F, size_t N>
    explicit fiber_context(F&& entry, span<byte, N> stack);

    ~fiber_context();

    fiber_context(fiber_context&& other) noexcept;
    fiber_context& operator=(fiber_context&& other) noexcept;
    fiber_context(const fiber_context& other) noexcept = delete;
    fiber_context& operator=(const fiber_context& other) noexcept = delete;

    fiber_context resume() &&;

    template<typename Fn>
    fiber_context resume_with(Fn&& fn) &&;

    bool can_resume() noexcept;
    explicit operator bool() const noexcept;
    bool empty() const noexcept;

    void swap(fiber_context& other) noexcept;
  };
}

P1061R4 Structured Bindings can introduce a Pack

構造化束縛可能なオブジェクトをパラメータパックに変換可能にする提案。

以前の記事を参照

このリビジョンでの変更は、CWGのレビューに伴うフィードバックを反映したことです。

P1708R7 Basic Statistics

標準ライブラリにいくつかの統計関数を追加する提案。

以前の記事を参照

このリビジョンでの変更は

  • オーバーロードを活用して、重み付きと重みなしの関数を呼び分けるようにした
  • 導出の表示を簡略化
  • 歪度と尖度の導出を追加
  • 文言の調整

などです。

P1715R1 Loosen restrictions on "t" typedefs and "v" values.

std::conditional_tの定義を修正する提案。

C++14で導入されたconditional_tは、std::conditional<B, T, F>::typeに簡易にアクセスするためのものです。それは次のような実装になるように指定されています

namespace std {
  template <bool B, class T, class F>
  struct conditional {
    using type = …;
  };

  template <bool B, class T, class F>
  using conditional_t = typename conditional<B, T, F>::type; // C++14
}

conditional_tconditional<B, T, F>::typeエイリアスでなくてはならないわけですが、このように指定していることがconditional_tのより効率的な実装を妨げています。

conditional_t<B, T, F>の現在の実装では、テンプレートパラメータB, T, F毎にstd::conditionalインスタンス化が必要となります。3つのパラメータのうちいずれか1つが異なっているだけで、std::conditionalの新しいインスタンス化が必要となります。これは、conditional_tを多用する環境において、コンパイル時間の増大やデバッグ情報の肥大化を招きます。

例えば、conditional_tの実装を次のように変更したとすると

template<bool _Bp>
struct __select;

template<>
struct __select<true>  {
  template<typename _TrueT, typename _FalseT>
  using type = _TrueT;
};

template<>
struct __select<false> {
  template<typename _TrueT, typename _FalseT>
  using type = _FalseT;
};

template <bool _Bp, class _TrueT, class _FalseT>
using conditional_t = typename __select<_Bp>::template type<_TrueT, _FalseT>;

この実装では、conditional_t<B, T, F>が異なるパラメータの組み合わせで何度使用されても、インスタンス化されるのは__select<true>__select<false>の2つのクラステンプレートだけです(エイリアステンプレートはインスタンス化されないため)。conditional_tがどれだけ多用されようともこの2つのクラステンプレートがインスタンス化された後はその定義を使いまわすことができ、最終的な型の決定においてはエイリアステンプレートの実引数による置換だけしか発生しません。これによって、コンパイラのメモリ使用量を抑えるだけでなく、デバッグのために出力するデバッグ情報に記録される型情報も削減することができます。

筆者の方の(Googleにおける)調査では、特にTMPが多用されているファイルに対してclangが出力するデバッグ情報の1部として記録されているクラス名の約1/6がstd::conditionalインスタンス化で占められていたそうです。

この提案は、これらの理由から、conditional_tの実装をstd::conditionalから切り離し、より効率的な実装を選択可能にするものです。

ただし、Google社内でこのような変更を行ったところ、この変更は観測可能であることが判明しています。

// 最初にこのように宣言され
template<bool B>
long to_long(conditional_t<B, int, long> param);

...

// その後でこのように定義されている(おそらく記述ミス)
template<bool B>
long to_long(typename conditional<B, int, long>::type param) {
  return param;
}

この時、conditional_tstd::conditionalによって定義されていない場合、この2つの関数宣言は異なるシグネチャを持つことになり、to_long()の呼び出しは2つのオーバーロードの間で曖昧となりコンパイルエラーを起こします。

ただし、この例が記述ミスを含むものであるように、このような例はかなり稀であるため実際の影響は非常に小さいと思われます。

P1759R5 Native handles and file streams

標準ファイルストリームに、OSやプラットフォームネイティブのファイルを示すものを取得する方法およびその型エイリアスを追加する提案。

以前の記事を参照

このリビジョンでの変更は、ほぼ設計と提案する文言のみに文書を絞ったこと、対象の型(.native_handle())を持つ型)としてstd::stacktrace_entryを考慮し、それを他のものと比較する記述を追記した事です。

C++23で追加されたスタックトレースの1行を表す型であるstd::stacktrace_entryもまた、その実装のハンドルを取得するために.native_handle()を持っています。ここから得られるネイティブハンドル型とstd::threadのそれとを比較して、ネイティブハンドル型について次のような要求を追加することを提案しています

  • native_handle_typesemiregularでありトリビアルコピー可能かつstandard_layout
  • ファイルのネイティブハンドルが何を意味しどのように動作するかを定義する

このことは、この提案の対象のファイルハンドルのnative_handle_typeに対してのみ要求されています。

P1854R4 Making non-encodable string literals ill-formed

文字列リテラルエンコーディングを実行時エンコーディングに変換する際、文字表現が失われる場合をコンパイルエラーとする提案。

以前の記事を参照

このリビジョンでの変更は、CWGのフィードバックを適用した事です。

この提案は、CWGのレビューを終えて次の全体会議で投票にかけられる予定です。

P1928R3 Merge data-parallel types from the Parallelism TS 2

std::simd<T>をParallelism TS v2から標準ライブラリへ移す提案。

以前の記事を参照

このリビジョンでの変更は

  • hmin()/hmax()の代替案を提案
  • <bit>との一貫性のために、simd_maskの削減を議論。曖昧さを避けるためによりよい名前を募集
  • some_ofを削除
  • simd_maskに単項~を追加
  • マスク付きオーバーロードの名前と引数順序について議論と回答を追加
  • fixed_size/resize_simdのNTTPをintからsize_tへ変更
  • ロード/ストアの変換について議論を追加
  • P2509R0を関連提案として追加
  • ロード/ストアをポインタからcontiguous_iteratorへと一般化
  • element_referenceの過剰な制約についてOpen questionsに移動

などです。

P2022R0 Rangified version of lexicographical_compare_three_way

std::lexicographical_compare_three_wayのRange版を追加する提案。

std::lexicographical_compare_three_wayは、与えられた2つのイテレータ範囲を辞書式順序で三方比較するイテレータアルゴリズムです。この関数はC++20で一貫比較とともに導入されたこともあり、対応するRangeアルゴリズムは用意されていませんでした。

この提案は、それを追加するものです。

// Rangeを受け取るものの宣言例
namespace std::ranges {

  template<
    ranges::input_range R1,
    ranges::input_range R2,
    class Comp = compare_three_way,
    class Proj1 = identity,
    class Proj2 = identity
  >
    requires is-lexicographical-compare-three-way-result-ordering<
               iterator_t<R1>, iterator_t<R2>, Comp, Proj1, Proj2
             >
  constexpr auto ranges::lexicographical_compare_three_way(
    R1&& r1,
    R2&& r2,
    Comp comp = {},
    Proj1 proj1 = {},
    Proj2 proj2 = {}
  ) -> common_comparison_category_t<
         decltype(
         comp(proj1(ranges::begin(r1)), proj2( ranges::begin(r2)))
         ),
         strong_ordering
       >;

}

他のRangeアルゴリズムと同様に、イテレータ範囲を受け取るものとそれをrangeで受け取るものの2種類が用意され、射影操作をサポートしています。is-lexicographical-compare-three-way-result-orderingというのは説明専用のbool定数の変数テンプレートで、それぞれの範囲の要素と比較関数オブジェクトcompによる比較結果が比較カテゴリ型を返すことを調べるものです。

P2047R6 An allocator-aware optional type

Allocator Awarestd::optionalである、std::pmr::optionalを追加する提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言やHTMLの調整です。

この提案はこれ以上議論されません。

P2159R1 A Big Decimal Type

Numbers TS (P1889R1)に対して、10進多倍長浮動小数点型std::decimalを追加する提案。

以前の記事を参照

このリビジョンでの変更はよくわかりませんが、SG6のレビューではこの提案の主張するユースケースに関心がないとして、これ以上議論しないことになったようです。

P2300R6 std::execution

P0443R14のExecutor提案を置き換える、任意の実行コンテキストで任意の非同期処理を構成・実行するためのフレームワークおよび非同期処理モデルの提案。

以前の記事を参照

このリビジョンでの変更は

  • 修正
    • get_completion_signaturesは一貫性のために、connectで使用されるものと同様のpromise型で待機可能性をテストするようにした
    • コルーチンpromise型は直接クエリするものではなく、環境プロバイダ(environment provider)(get_env()を実装するもの)であることを明確化
  • 機能拡張
    • senderクエリはsenderget_attrs()に渡すことによってアクセスされる、個別のクエリ可能な属性オブジェクトに移動される
      • senderコンセプトは、get_attrs()を必要とするように再表現され、ある型が特定の実行環境内でsenderであるかをチェックするsender_in<Snd, Env>コンセプトから分離されている
    • プレースホルダno_envdependent_completion_signatures<>は不要になったため削除された
    • 入力senderget_attrs()を呼び出した結果を永続化するために、ensure_startedsplitを変更
    • schedulerreceiverコンセプトの定義を制約の再帰を回避するように修正
    • sender_ofコンセプトをより人間工学的で汎用なものに再表現
    • エイリアステンプレートvalue_types_of_terror_types_of_tの指定、及び変数テンプレートsends_doneを、新しい説明専用エイリアステンプレートgather-signaturesを使用することで簡潔にした

などです。

この提案での大きな変更は、senderが直接クエリ可能ではなくなり、get_attrs()を介して個別のクエリCPOによってクエリを行うように変更されたことです。以前は、schedulerreceiversenderの3つのものは全て直接クエリ可能でした(クエリCPOに直接渡せた)。R4でreceiverのクエリは別の環境オブジェクトを介する形に変更され、それはreceiverget_env()に渡して取得できます。環境オブジェクトを介するようにしたのは、型の再帰が起こるのを回避するためでした。

このリビジョンでは、senderに関しても同様に属性オブジェクトを介して各種クエリCPOに渡して各種特性をクエリするように変更されました。これは、splitsecure_startedアルゴリズムの設計上の問題解決のためのようです。

クエリCPOをQsenderオブジェクトをsreceiverオブジェクトをr、クエリのための追加の引数をargs...とすると、sender/receiverに対するクエリは次のように行えます

// senderのクエリ
Q(get_attrs(s), args...);

// receiverのクエリ
Q(get_env(r), args...);

Qとしては、std::get_allocator(関連づけられたアロケータを取得)やstd::get_stop_token(関連づけられたstop_tokenを取得)、std::execution::get_scheduler(関連づけられたschedulerを取得)などがあります。クエリとは、schedulerreceiversenderなどに対してその実行環境に関する情報や実行時に使用するものなどを問い合わせ、取得するための操作です。

このリビジョンでもまだschedulerは直接クエリ可能であり、それを変更しようとする動機は今のところ無いようです。またそのほかに、operation_statereceiversenderconnectして得られるもの)もこのリビジョンで直接クエリ可能とされています。

この提案は現在LWGでのレビュー中です。

P2308R0 Template parameter initialization

非型テンプレートパラメータの初期化に関しての規定を充実させる提案。

現在に至るまで、非型テンプレートパラメータ(NTTP)の初期化に関しての規定は、指定された初期化子がNTTPの型に変換可能であること、及び、その変換は定数式であること、くらいしか指定されていませんでした。それでも、C++17まではNTTPに取れるのは一部の組み込み型の値に限られていたたためあまり問題にはならなかったようです。

しかし、C++20から非型テンプレートパラメータとしてクラス型のオブジェクトを扱うことができるようになりました。NTTPとして扱えるクラス型には制限があるものの、コンストラクタを持つことができる他ポインタ型のメンバを持つこともできます。すると、左辺値NTTPをとるクラステンプレートの初期化時にそのアドレスを調べることができ、それによってある種のパラドックスが発生します。

template<auto n>
struct B { /* ... */ };

struct J1 {
  J1* self = this;
};

B<J1{}> j1; // ??

このJ1自体はNTTPで使用可能なクラス型で、その初期化も問題なさそうに思えます。しかし、J1::selfthisによってデフォルト初期化されており、J1{}とすると初期化にあたって自身のアドレスを要求します。普通の変数としておいた場合などではこれは問題にはならないのですが、ことNTTPだとこれが深刻な問題となります。これは簡単に言えば、J1thisを決めるためにはまずそのNTTPを持っているテンプレートがインスタンス化されなければならず、テンプレートがインスタンス化するためには全てのNTTPの初期化が完了しなければなりません。

これは、テンプレートはインスタンス化に際して(そのオーバーロードの適切な処理、あるいはODRのために)テンプレートパラメータ毎の同一性を判定する必要があり、NTTPの場合はその値の同一性によって判定され、クラス型のNTTPの場合その型名及び全てのメンバの値によって同一性が判定され、ポインタ型の同一性はそのアドレスによって判定されるためです。

現在の(C++20時点の)規定はこのようなことを考慮しておらず、このNTTP初期化に伴う矛盾を解決することができません。

この提案は、この問題を含むNTTPの初期化に関する規定を適切に書き直すことで、いくつかのコア言語Issueを解決するものです。上記問題の他にも、{}初期化がクラス型NTTPで使えるのかどうか不透明な問題も解決を図っています。

この提案によるアプローチではまず、テンプレート実引数で使用可能な構文(template-argument)として{}初期化子(braced-init-list)を許可します。

その上で、プレースホルダ型(auto)あるいはテンプレートパラメータを推論する必要のある形で宣言(C++17 CTAD)されているNTTPの型の推定について次のように変更します。そのような推論を必要とする型名/プレースホルダauto)を仮にDとすると

// 現在
D x = template-argument;

// この提案
D x = E;

ここで、Etemplate-argumentかデフォルト引数に指定されている{}初期化子のいずれかの式です。このような仮のxの初期化式を構成し、この時にxの型として推論される型をそのNTTPの型(仮にTとする)として推定します。

このようにすることで、NTTPのデフォルト引数も含めてNTTPの実引数として{}初期化子が使用できることを明示的にしています。

次に、NTTPの初期化においては、まず模範(exemplar)となる値をその初期化式(NTTPの実引数A)から決定します。模範となる値の型UTもしくはTが参照型ならその参照される型として

  • Uがクラス型ではなく、Aが波括弧初期化ではない場合
    • 模範となる値は、定数式でATへ変換した値
  • それ以外の場合
    • const U v = A;と初期化される一時変数vを導入して
    • 模範となる値は、v

そして、NTTPは模範となる値からコピー初期化(copy-initialization)されます。

Uがクラス型の場合、NTTPの同一性は模範となる値vによって決定されます。

このように、NTTPの初期化のための一時変数(模範となる値)を初期化して、それを用いてNTTPの同一性を判定し、またNTTPの値はそこからコピーして初期化することで、まずテンプレートの同一性が判定されてから初期化が起こるようにするとともに、上記J1メンバselfのような例では一時オブジェクトのアドレスを保持してしまうためエラーとなるようになります。

提案より、サンプルコード

template<int i>
struct C { /* ... */ };

C<{ 42 }> c1;  // OK、波括弧初期化の許可

struct J1 {
  J1* self = this;
};

B<J1{}> j1;  // error: J1::selfが一時オブジェクトのアドレスを取っている

struct J2 {
  J2* self=this;
  constexpr J2() {}
  constexpr J2(const J2&) {}
};

B<J2{}> j2;  // error: NTTPの初期化後に模範となる値と異なる値が生成される(コピーコンストラクタの呼び出しによる)

P2338R4 Freestanding Library: Character primitives and the C library

<charconv>std::char_traitsをはじめとするいくつかのヘッダをフリースタンディングライブラリ指定する提案。

以前の記事を参照

このリビジョンでの変更は、非推奨とされたerrc/errnoを取り除いたことです。

この提案は既にLWGでのレビューを終えており、次の全体会議で投票にかけられる予定です。

P2355R1 Postfix fold expressions

可変長テンプレートの畳み込み式において、() []の2つの演算子を使用可能にする提案。

以前の記事を参照

このリビジョンでの変更は

  • pack[...[abc]]のような一貫性のない畳み込みを禁止
  • インデックスの例の構文を修正
  • pack[...][expr]exprにおける式としてassign-or-braced-init-listの代わりにinitializer-clauseを指定

などです。

P2361R6 Unevaluated strings

コンパイル時にのみ使用され、実行時まで残らない文字列リテラルについての扱いを明確化する提案。

以前の記事を参照

このリビジョンでの変更は、CWGレビューに伴うフィードバックの反映です。

この提案は既にCWGのレビューを終えており、次の全体会議で投票にかけられる予定です。

P2363R5 Extending associative containers with the remaining heterogeneous overloads

連想コンテナの透過的操作を、さらに広げる提案。

以前の記事を参照

このリビジョンでの変更は、LWGのフィードバックを反映した事です。

この提案はLWGのレビューを完了しており、次の全体会議で投票にかけられる予定です。

P2406R3 Add lazy_counted_iterator

P2406R4 Add lazy_counted_iterator

P2406R5 Add lazy_counted_iterator

std::counted_iteratorを安全に使用可能にする提案。

以前の記事を参照

R3での変更は

  • counted_iteratorとは異なり、iterator_concept/iterator_categoryを定義した

R4での変更は

  • input_or_output_iteratorからinput_iteratorへの変更忘れを適用
  • 2つのイテレータが同じシーケンスを参照する場合の単純化
  • 後置++の定義を単純化
  • 追加の設計や疑問点をまとめたセクションを追加
  • 実装経験リンクの追加

このリビジョンでの変更は

  • LEWGでの投票結果の追記
  • 代替設計について追記
  • 機能テストマクロの追加

などです。

この提案は、P2799のソリューションによって置き換えられるようで、議論は停止されています。

P2495R2 Interfacing stringstreams with string_view

std::stringstreamstd::string_viewを受けとれるようにする提案。

以前の記事を参照

このリビジョンでの変更は

  • LWGのガイダンスに従って、クラスごとにオーバロードされたコンストラクタの文言をマージした
  • typenameの代わりにclassを使用する
  • 文言のEffects節のスタイルの調整

などです。

この提案は現在C++26をターゲットとしてLWGのレビュー中です。

P2497R0 Testing for success or failure of charconv functions

std::to_chars_result/std::from_chars_resultに成否を簡単に問い合わせるためのboolインターフェースを追加する提案。

std::to_chars_result/std::from_chars_resultstd::to_chars()/std::from_chars()の結果型で、std::errcとポインタの2つのメンバを持っています。

多くの場合、それらの結果を構造化束縛で受けて、メンバのerrcオブジェクトをstd::errc{}(デフォルト値、成功を表す)と比較することで処理の成否を判断するコードが書かれます。

// 42を文字列へ変換し範囲[p, last)へ書き込む
auto [ptr, ec] = std::to_chars(p, last, 42);

if (ec == std::errc{}) {
  // 成功時の処理
  ...
}

std::errcは単なるスコープ付き列挙型(enum class)でしかなく、これ以上に良い書き方は現状ありません。しかし、この比較は少し冗長かつ煩雑で、より読みやすい成功判定方法が求められました。

この提案はそのために、両結果型にoperator bool()を追加して改善を図るものです。

// 42を文字列へ変換し範囲[p, last)へ書き込む
auto [ptr, ec] = std::to_chars(p, last, 42);

if (ec) {
  // 成功時の処理
  ...
}

// あるいは
if (std::to_chars(p, last, 42)) {
  // 成功時の処理
  ...
}

// form_chars()も同様
if (int v; std::from_chars(p, last, v)) {
  // 成功時の処理
  ...
}
namespace std {
  struct to_chars_result {
    char* ptr;
    errc ec;
    
    friend bool operator==(const to_chars_result&, const to_chars_result&) = default;
    
    // 追加
    constexpr explicit operator bool() const noexcept { return ec == errc{}; }
  };

  struct from_chars_result {
    const char* ptr;
    errc ec;

    friend bool operator==(const from_chars_result&, const from_chars_result&) = default;
    
    // 追加
    constexpr explicit operator bool() const noexcept { return ec == errc{}; }
  };
}

この提案はすでにLWGのレビューを終えており、C++26ターゲットとして次の全体会議で投票にかけられる予定です(事務手続きのミスによりC++23にまにあわなかったとのこと・・・)

P2521R3 Contract support -- Record of SG21 consensus

C++に最小の契約プログラミングサポートを追加する提案。

以前の記事を参照

このリビジョンでの変更は、

  • タイトルの変更
  • SG21における副作用の許可に関する決定を追記
  • 契約条件式からの例外送出についての問題を追記

などです。

2月のIssaquah会議においてSG21は、契約条件に含まれる副作用を認めるとともに、プログラムがそれに依存しないように、契約条件は0回以上呼ばれる可能性があるとすることを決定したようです。これによって、これ以外の副作用の方針(副作用の完全禁止、評価の内側に止まっているもののみ許可、など)は否決されました。

また、現在のところ、契約条件がその評価時に例外を投げた場合にどう扱うかは決まっていません。ある提案ではstd::terminate()を呼び出して終了することが提案されていましたが否決されており、別の提案では例外送出を契約違反として扱うことが提案されています。

P2527R2 std::variant_alternative_index and std::tuple_element_index

std::variantに対して、型からそのインデックスを取得するための方法を追加する提案。

以前の記事を参照

このリビジョンでの変更は提案する文言の改善のみです。

P2545R3 Why RCU Should be in C++26

標準ライブラリにRead-Copy-Update(RCU)サポートを追加する提案。

以前の記事を参照

このリビジョンでの変更は

  • メモリリーク検出機の使用についてユーザーガイドを追加
  • 非同期コードとの対話のためのユーザーガイドを追加
  • rcu_obj_baseの説明から「クライアントが提供するテンプレート引数」という言葉を削除
  • rcu_obj_baseに5つのコンストラクタ、代入演算子、デストラクタを追加
  • rcu_obj_baseのテンプレートパラメータはrcu_obj_baseが参照される前に完全型でなければならないようにした
  • .retire(), .unlock(), rcu_retire()に関するコメントをNoteに移動
  • そのた文言の調整や改善

などです。

この提案はLEWGでのレビューを終えて、LWGに転送されています。

P2558R2 Add @, $, and ` to the basic character set

@ $ `の3種類の文字をソースコードの基本文字集合に追加する提案。

以前の記事を参照

このリビジョンでの変更は、よくわかりません。

この提案は既にCWGのレビューを終えて、次の全体会議で投票にかけられる予定です(C++26ターゲットです)。

P2572R1 std::format() fill character allowances

std::formatにおいて、文字列のアライメント(左寄せ、中央寄せ、右寄せ)の際に空白を埋める文字として使用可能な文字を制限する提案。

以前の記事を参照

このリビジョンでの変更は

  • 既存実装の振舞いとして、GCC13での<format>実装による出力(エラー)の例を追加
  • フィールド幅の単位(field width unit)、最小フィールド幅(minimum field width)、推定フィールド幅(estimated field width)、パディング幅(padding width)の正式な定義の追加
  • ↑のための調整
  • alignオプションとの一貫性向上のため、0オプションの文言の調整
  • std-format-specという文法要素を参照するための一貫した用語の導入
  • 22.14.2.2 [format.string.std]/11に対して変更していた、コードポイントという用語のUCSスカラ値への変更を削除
  • 文言変更の意図を明示的にするためにドラフトメモを追加
  • その他フィードバックの適用

などです。

この提案は、2月のIssaquah会議で全体投票をパスしてC++23に適用されています。

P2588R3 Relax std::barrier phase completion step guarantees

std::barrierのバリアフェーズ完了時処理が、同じバリアで同期する任意のスレッドから起動できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、LWGのフィードバックに基づく文言の修正、機能テストマクロの追加、Annex Cセクション(C++20との非互換)の追記、などです。

この提案は、2月のIssaquah会議で全体投票をパスしてC++23に適用されています。

P2591R3 Concatenation of strings and string views

std::stringstd::string_view+で結合できるようにする提案。

以前の記事を参照

このリビジョンでの変更は読みやすさの改善のみです。

P2592R3 Hashing support for std::chrono value classes

<chrono>の時間や日付を表す型に対してハッシュサポートを追加する提案。

以前の記事を参照

このリビジョンでの変更は、LWGのレビュー受けての文言の修正です。

この提案はLWGのレビューを終えて、次の全体会議で投票にかけられる予定です。

P2593R1 Allowing static_assert(false)

static_assert(false)がテンプレートの実体化前にエラーとならないようにする提案。

以前の記事を参照

このリビジョンでの変更は、これを許可するためのセマンティクスとして考えられる他の方法を追記したことです。

この提案は、2月のIssaquah会議で全体投票をパスしてWDに適用されており、C++11へのDRとなっています。

P2594R1 Slides: Allow programmer to control coroutine elision (P2477R3 Presentation))

P2477(コルーチンの動的メモリ確保章竜最適化の制御のための機能の提案)の解説スライド

このリビジョンでの変更はおそらく、P2477の更新に伴う内容の更新です。

P2609R2 Relaxing Ranges Just A Smidge

P2609R3 Relaxing Ranges Just A Smidge

射影(プロジェクション)を取るアルゴリズムについて、その制約を緩和する提案。

以前の記事を参照

R2での変更は、indirect_value_tがネストした射影をハンドルできることを明確にしたことなどです。

このリビジョン(R3)での変更は、提案する文言の修正のみです。

この提案は、2月のIssaquah会議で全体投票をパスしてC++23に適用されています。

P2616R4 Making std::atomic notification/wait operations usable in more situations

std::atomicnotify_one()wait()操作を使いづらくしている問題を解消する提案。

以前の記事を参照

このリビジョンでの変更は

  • 既存のnotification/wait操作の非推奨化をやめた
  • notify_token::notify_one/allの生存期間に関する言及をNoteに移動
  • P2689R1の文言を含めるようにした(採択される場合)

などです。

P2621R2 UB? In my Lexer?

字句解析するだけで未定義動作を引き起こすものについて、未定義ではなくする提案。

以前の記事を参照

このリビジョンでの変更は、CWGのレビューによるフィードバックの適用です。

この提案は既にCWGのレビューを終えており、C++26に向けて次の全体会議で投票にかけられる予定です。

P2641R2 Checking if a union alternative is active

定数式において、unionのどのメンバがアクティブメンバかを調べるためのstd::is_active_member()の提案。

以前の記事を参照

このリビジョンでの変更は、提案する機能をunionのアクティブメンバ検出に限ったものではなく、定数式であるオブジェクトが生存期間内にあるかどうかを調べるstd::is_within_lifetime()に変更されたことです。

unionの特定のメンバがアクティブメンバであるかを問い合わせるということは、オブジェクトが生存期間内にあるかを問い合わせることの特殊なケースです。以前のstd::is_active_member()をそこまで一般化させても実装可能性やこの提案の元の同期に対してデメリットもないと判断されたため、アクティブメンバのチェックから特定オブジェクトの生存期間チェックに対象を広げ、関数名もstd::is_within_lifetime()に変更されました。

とはいえ、使用感はほぼ同じで宣言も変化しません。

namespace std {
  template<class T>
  consteval bool is_within_lifetime(T*) noexcept;
}

提案にあるOptBoolの例も以前とこの関数名以外変わりありません。

struct OptBool {
  union { bool b; char c; };

  constexpr OptBool() : c(2) { }
  constexpr OptBool(bool b) : b(b) { }

  constexpr auto has_value() const -> bool {
    if consteval {
      return std::is_within_lifetime(&b); // 定数式ではbがアクティブメンバであるか(生存期間内にあるか)を問い合わせる
    } else {
      return c != 2;  // 実行時は今まで通り
    }
  }

  constexpr auto operator*() -> bool& {
    return b;
  }
};

P2652R2 Disallow user specialization of allocator_traits

std::allocator_traitsのユーザーによる特殊化を禁止する提案。

以前の記事を参照

このリビジョンでの変更は、allocate_at_least()allocator_traitsの非テンプレートメンバではなくネストしたテンプレートメンバとなっていたのを修正したことです。

この提案は、2月のIssaquah会議で全体投票をパスしてC++23に適用されています。

P2655R2 common_reference_t of reference_wrapper Should Be a Reference Type

P2655R3 common_reference_t of reference_wrapper Should Be a Reference Type

std::reference_wrapper<T>T&の間のcommon_referenceT&になるようにする提案。

以前の記事を参照

R2での変更は、CV修飾されたプロクシ型のcommon_referenceに関する問題を修正したことなどです。

このリビジョン(R3)での変更は、新しい機能テストマクロ(__cpp_lib_common_reference)を追加し、古いものをリネーム(__cpp_lib_common_reference_wrapper)したことです。

この提案は、2月のIssaquah会議で全体投票をパスしてC++23に適用されています。

P2656R2 C++ Ecosystem International Standard

C++実装(コンパイラ)と周辺ツールの相互のやり取りのための国際規格を発効する提案。

以前の記事を参照

このリビジョンでの変更は、SG15での投票を受けて、最初のISの目標を絞り、それらについての解説を追記したことです。

このリビジョンでの最初のISの目標は次の6項目です

  1. 定義
    • 標準の仕様を記述し、またその範囲を制限するための言葉や概念などの定義
  2. ビルドシステムとパッケージマネージャの相互運用のための基盤
    • ビルドシステムとパッケージマネージャが相互に対話するためのメッセージのフォーマットとインターフェースの仕様
  3. ファイル拡張子
    • ツールが認識し理解する必要のあるファイル拡張子の最小セットと、その役割
  4. イントロスペクション
    • ツールがサポートするEcosystem ISのバージョンを問い合わせ、回答するためのフォーマットとインターフェースの仕様
  5. ポータブルな診断メッセージのフォーマット
    • ツールが出力するエラーメッセージ等のユーザーに出力するメッセージのフォーマット
    • SARIFという形式を取り込むことを目指す
  6. コマンドラインの移植性
    • 異なるツールやプラットフォームの間で認識可能な、ツールコマンドを表現する共通言語
    • C++コンパイルの際の最適な通信手段となる、標準的な構造化応答ファイル形式を定義することを目標とする

これによって、示されているタイムラインの第一段階(計画の策定)はクリアしたことになりそうです。

P2663R1 Proposal to support interleaved complex values in std::simd

std::simdstd::complexをサポートできるようにする提案。

以前の記事を参照

このリビジョンでの変更は、SG1での関連する提案の投票結果を追記したことです。

P2664R1 Proposal to extend std::simd with permutation API

Parallelism TS v2にあるstd::simdに、permute操作のサポートを追加する提案。

以前の記事を参照

このリビジョンでの変更は、SG1での関連する提案の投票結果を追記したことです。

P2670R1 Non-transient constexpr allocation

定数式で確保したメモリ領域を実行時に持ち越すことをできるようにする提案。

以前の記事を参照

このリビジョンでの変更は、propconst修飾子の代わりにpropconst指定子を説明し、それをメインの提案としたことです。

以前に提案されていたpropconst修飾子(qualifier)は、現在メンバ変数宣言に対してconstを指定する場所に置くことができ、そのポインタのconst性がdeep constとなるようにするものです。対して、propconst指定子(specifier)は、現在メンバ変数宣言に対してmutableを指定する場所に置くことができ、意味は同じになります。

指定子であることの利点は、多重ポインタに対してどこがpropconstなのかを指定できるようになることです。

propconst修飾子の場合、int propconst**int propconst* propconst*のように指定することができます。このとき、propconst int** pという宣言はpconstとしてアクセスされる際の意味として、次のような候補があります(propconstの役割から、const性は参照先のみを考慮する)

  • int const* const* (全てのレイヤでconst
  • int const** (1層目のみconst
  • int* const* (2層目のみconst

しかし、int const**int**に変換できないため、最後の選択肢のみが残ります。

その上で、std::vector<T>の実装を考えてみます。std::vector<T>::data() constT const *を返しますが、T自体がポインタ型(T*)の場合はT* const *を返します。これはT* constへのポインタであり、std::vector<T*>の要素であるポインタへのポインタかつ、要素がconst修飾されています。複雑ではありますが、意図通りになっています。

std::vector<T>の所有するストレージへのポインタ(T* begin_とする)をpropconst指定(propconst T* begin_)したとき、全てのレイヤにconstを付加するとすると、T const* const*が得られ、これは本来のT* cosnt*に変換できません。これによって、std::vector<T*> const後方互換を破壊し、実質的に使用できなくなってしまいます。

このことは、propconstconstを適用するのは1番外側のみであることを示唆しています。例えば、propconst int** pint* const*になり、propconst int*** qint** const*になります。

この場合に、簡単な行列型の実装を考えてみます。

struct Matrix3D {
  propconst int*** p;
  int n;

  constexpr ~Matrix3D() {
    for (int i = 0; i != n; ++i) {
      for (int j = 0; j != n; ++j) {
        delete [] p[i][j];
      }
      delete [] p[i];
    }
    delete [] p;
  }
};

この場合、Matrix3D::pint** const*として扱われます。すると、pそのものやp[i]は変化できませんが、真ん中のレイヤは変更可能になります(例えば、p[0][0] = new int(42);)。これによって、このクラスは定数式で使用することができません。

この例の場合は、外側のレイヤのみではなく一番内側を除く全てのレイヤでconstが必要になります。

宣言 外側のみconst 一番内側以外const
propconst int* int const* int const*
propconst int** int* const* int* const*
propconst int*** int** const* int* const* const*
propconst int**** int*** const* int* const* const* const*

しかし、std::vector<T**>ではT** const*となる必要があり、この方法もうまくいかないことがわかります。

結局、propconst1つでこれらのユースケースを全て満足することはできません。

そこで、propconst指定子によって、追加するconstのレイヤ数を指定できるようにすれば、ほぼ同じ構文によってどこまでconstを追加するのかを選択できるようになります。

宣言 意味
propconst int int
propconst int* int const*
propconst(1) int* int const*
propconst(2) int* int const*
propconst int** int const* const*
propconst(1) int** int* const*
propconst(2) int** int const* const*
propconst(3) int** int const* const*
propconst int*** int const* const* const*
propconst(1) int*** int** const*
propconst(2) int*** int* const* const*
propconst(3) int*** int const* const* const*

このpropconst指定子を使用する場合、std::vectorpropconst(1)を常に使用し、先ほどのMatrixNDのようなクラスはpropconst(N)(もしくは単にpropconst)を使用することで、const付加位置の問題を解決し、定数式でも使用可能となります。

また、propconst修飾子の場合、std::unique_ptr<propconst T>のような宣言が行えてしまい、この時にメンバ関数が正しく動作するためにはメンバの宣言を変更する必要があります。これは、std::tupleで使用すると、const伝播tupleとして使用できるメリットもありますが、より広い部分での後方互換の考慮と既存ライブラリの変更が必要になります。

propconst指定子の場合そのような使い方はできず、現在のmutableとほぼ同様の使い方しかできません。しかし、そのメリット(本当にメリットかどうかはわかりませんが)が得られないものの、propconstの本来の目標である定数式で確保したメモリの実行時への持ち越し(非一時的なメモリ割り当て)の許可の大部分を達成できており、その導入に伴って考慮すべきスコープを狭めています。

これらの理由から、この提案では、R0のstd::mark_immutable_if_constexpr()でもpropconst修飾子でもなく、propconst指定子を非一時的なメモリ割り当ての許可のための言語機能として提案しています。

P2679R2 Fixing std::start_lifetime_as and std::start_lifetime_as_array

std::start_lifetime_asの既存機能等との非一貫性を正す提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の修正のみです。

この提案は、2月のIssaquah会議で全体投票をパスしてC++23に適用されています。

P2681R1 More Basic Statistics

標準ライブラリにいくつかの統計関数を追加する提案。

以前の記事を参照

このリビジョンでの変更は、線形回帰が将来の別の提案へ遅延された(より大きな、回帰分析ファミリーの一部であるため)ことです。

P2693R1 Formatting thread::id and stacktrace

std::thread::idstd::stacktracestd::format()及びstd::print()で出力できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、LWGからのフィードバックの適用と、stacktrace_entryフォーマットのfillオプションの必要性についてのLEWGへの質問を追記した事です。

以前のリビジョンから、stacktrace_entryのフォーマットではfillオプションが有効とされていました。これは有用である可能性があり、後から追加すると(std::formatterに状態を追加する必要があるため)ABI破壊を招くことから、そのままにすることがLEWGによって決定されています。

この提案は、2月のIssaquah会議で全体投票をパスしてC++23に適用されています。

P2695R1 A proposed plan for contracts in C++

C++ Contracts(契約プログラミング)のC++26導入へ向けた予定表。

以前の記事を参照

このリビジョンでの変更は、契約機能の構文決定(R0では2023.02)と契約違反時のサポートするビルドモードの決定(R0では2023.03)の予定を入れ替えたことです。

P2724R1 constant dangling

現在ダングリング参照を生成しているものについて、定数初期化可能なものを暗黙的/明示的に定数初期化する提案。

以前の記事を参照

このリビジョンでの変更は、関連する提案へのリンクを追加したことです。

P2727R1 std::iterator_interface

イテレータを簡単に書くためのヘルパクラスの提案。

以前の記事を参照

このリビジョンでの変更は

  • 基底クラスの入れ子型が派生クラスでも定義されているかに関する議論を追加
  • 名前の候補を追加

などです。

P2730R1 variable scope

ローカルスコープの一時オブジェクトの寿命を文からスコープまで延長する提案。

以前の記事を参照

このリビジョンでの変更は、関連する提案へのリンクを追加したこと、TemporariesDesign Alternativesのセクションを追加したことです。

P2733R1 Fix handling of empty specifiers in std::format

P2733R2 Fix handling of empty specifiers in std::format

P2733R3 Fix handling of empty specifiers in std::format

std::format()のフォーマット文字列内の置換フィールドが空である場合に、パースを行わなくてもよくする提案。

以前の記事を参照

R1での変更は、ネストしたrange/tupleのフォーマットバグ解消のための代替案の比較を追記したことです。

R2での変更は

  • オプションなし({}{:})の場合にフォーマット文字列解析をスキップする許可を削除
  • tuple要素のフォーマッターのためのparse()呼び出しを追加
  • フォーマット文字列({}:の右側)は{から始まることができないことを明確化
  • set_debug_formatの文言改善

このリビジョン(R3)での変更は

  • 実装可能性の問題の発覚のため、ネストしたrange/tupleのフォーマットバグ解消の解決方法を変更した事です。

ネストしたrange/tupleのフォーマットバグというのは、rangerangetupletupleなどに対するフォーマットにおいて、その要素(内側のrange/tuple)に対してデバッグ出力を有効化することができない(本来はするべきだったが実装不可能になっていた)問題の事です。

auto s = fmt::format("{}", std::make_tuple(std::make_tuple('a')));
// Before : ((a))
// Aftter : (('a'))

以前のリビジョンではこの解決のために、ネストした要素型でデバッグ出力が可能な場合にset_debug_format()を正しく呼ぶように規定、のようなことをしていましたが、その実装が難しいことから、rangetupleのフォーマッターにset_debug_format()を追加し、そのふparse()set_debug_format()を常に呼ぶ、のような基底に変更されました。

この提案は、元の目的(空のフォーマット文字列解析の省略)を失っているためこれ以上追及されないようです。ネストしたrange/tupleのフォーマットバグについては、ここでの知見を基にした別のIssue報告によって解決するようです。

P2736R2 Referencing the Unicode Standard

ISO 10646(UCS)の代わりにユニコード標準を参照するようにする提案。

以前の記事を参照

R1での変更は、SG16フィードバックによる文言の改善です。

このリビジョンでの変更は、SG16フィードバックによる文言の改善とP2713R1との文言衝突をマージした事です。

この提案は、2月のIssaquah会議で全体投票をパスしてC++23に適用されています。

P2738R1 constexpr cast from void*: towards constexpr type-erasure

定数式において、void*からポインタ型への変換を許可する提案。

以前の記事を参照

このリビジョンでの変更は、文言の修正のみです。

この提案は現在、CWGでレビュー中です。

P2740R1 Simpler implicit dangling resolution

P2740R2 Simpler implicit dangling resolution

関数からローカル変数の参照を返してしまうケースをコンパイルエラーにする提案。

以前の記事を参照

R1での変更は、文章の修正です。

このリビジョンでの変更は、関連する提案へのリンクを追加したことです。

この提案は、SG23のレビューでこれ以上時間をかけないことが決定されています。

P2741R1 user-generated static_assert messages

static_assertの診断メッセージ(第二引数)に、コンパイル時に生成した文字列を指定できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、コンパイル時の文字エンコーディングに関する議論の拡充と、volatile型に関する文言の削除です。

P2742R1 indirect dangling identification

P2742R2 indirect dangling identification

戻り値の参照やポインタの有効期間が別の変数の生存期間に依存していることを表明する属性の提案。

以前の記事を参照

R1での変更は、文章の改善とTooling Opportunitiesセクションを追加したことです。

このリビジョンでの変更は、関連する提案へのリンクを追加したことです。

この提案は、SG23のレビューでこれ以上時間をかけないことが決定されています。

P2749R0 Down with "character"

規格署中で使用されるcharacterという言葉を正しく使用しなおす提案。

規格署中で使用されるcharacterという言葉は多くの場所で不正確かつ曖昧な用法となっており、また、translation setというC++の規格署でしか使用されない言葉の使用にも問題があります。この提案は、適切な技術用語を使用してそれらを置き換えることで、表現を明確にするとともに解釈を容易にし、誤った意味にとられないようにするものです。

この提案では主に、単体あるいは近い形で使われているcharacterという言葉をUnicodeの用語(Unicode scalar value/Unicode code point)によって置き換えようとしています。一方、文字リテラルや文字型などのC++の要素を指す言葉や、改行文字など既に明確なものは変更しようとはしていません。

この変更は言葉遣いの正確さを向上させるためのもので、何か動作の変更を意図したものではありません。そのため、ほとんどのプログラマには影響はないでしょう。

P2750R1 C Dangling Reduction

P2750R2 C Dangling Reduction

Cの言語機能の範囲で発生するダングリングポインタを抑制する提案。

以前の記事を参照

R1での変更は、文章の修正です。

このリビジョン(R2)での変更は、サンプルコードの修正です。

この提案は、SG23のレビューでこれ以上時間をかけないことが決定されています。

P2751R1 Evaluation of Checked Contracts

契約条件のチェックに関する細かいルールの提案。

以前の記事を参照

このリビジョンでの変更は

  • 提案の2.3、2.4、3.4を明確化

    • 2.3 : 契約条件式の評価時に例外が投げられる場合は契約違反が発生している
      • 契約条件評価時に例外が伝播することを許可すると、noexceptが付加されている関数が契約条件をもつ場合、その関数評価を含むような式を渡したnoexcept演算子は何を返すべきか?
      • 想定される振る舞いには、様々な影響があり、それを考慮しなければならない
    • 2.4 : 契約条件式がUBを含む場合、欠陥が発生する
      • UBによるタイムトラベル(制御フローパスの破棄)が契約条件内で発生する場合、違反ハンドラの呼び出しに置き換えることを意図している
    • 3.4 : 複数の契約条件が連続して評価される場合、それらは相互に並べ替えて評価される可能性がある
      • 並べ替えられた契約条件の評価は、0ではない回数評価される可能性がある
  • P2751 進行状況

P2754R0 Deconstructing Avoiding Uninitialized Reads of Auto Variables

未初期化自動変数のゼロ初期化に関するいくつかのソリューションをまとめ、比較する提案。

この提案は、P2723R0の提出をうけて寄せられたいくつものフィードバックから特にその代替手段について調査し、それらの特徴や利点を比べることで、P2723とその問題の議論を促進しようとするものです。

P2723R0については以前の記事を参照

P2723R0の目標は、非クラス型の自動変数を初期化する前に不定の値を読み取ることによるセキュリティリスクを排除することです。P2723でもこの提案でも対象は未初期化の自動変数のみであり、その他のもの(動的確保したメモリや構造体のパディングなど)については考慮していません。

提案では、問題を理解するために、ごく簡単な未初期化変数読み取りの例を記載しています

// 無条件でUB
void f1() {
  int p;
  int q = p + 1;  // UB
}

// 条件次第でUB
void f2() {
  int y;
  int z = b ? y + 1 : 0;
}

// 未初期化変数を別の関数に渡す
void g3(int);

void f3() {
  int x;
  g3(x);  // likely a bug
}

// 未初期化である可能性のある変数を別の関数に渡す
void g4(int);
void f4() {
  int s;
  if (c) s = 0;

  g4(s);  // likely a bug
}

// 未初期化である可能性のある変数を別の関数に参照渡しする
void g5(int*);
void f5() {
  int t;

  g5(&t);  // possibly a bug
           // g5()がtに出力のみを行うならUBではない
}

// 意図的な初期化の遅延
void f6() {
  // スタック領域を使用するアロケータ
  char buffer[1000];
  BufferAllocator a(buffer, sizeof buffer);
  // vectorで使用されることで初期化される
  std::vector v(&a);

  // すぐに別の値を書き込むことが分かっているため初期化しない
  char buffer2[1000];
  snprintf(buffer2, sizeof buffer2, "cstring");
}

// テンプレートパラメータ型の変数宣言
template <typename T>
void f7() {
  
  T t;  // クラス型の場合は初期化され、組み込み型の場合は未初期化

  cout << t;  // 組み込み型の場合UB
}

この提案で挙げられているこのような問題の解決策の候補は次の7つです

  1. 常にゼロ初期化
    • 非クラス型の自動変数が初期化されない場合、常にゼロ初期化される
  2. ゼロ初期化もしくは診断
    • 無条件に不定値を読む場合は診断(コンパイルエラー)
    • 条件次第で不定値を読む可能性がある場合はゼロ初期化
  3. ソースでの初期化を強制
    • 非クラス型の未初期化変数はill-formed
  4. 後から初期化されることを考慮しつっつ、ソースでの初期化を強制
    • 注釈なしの非クラス型の未初期化変数はill-formed
    • 未初期化変数は明示する
  5. 実装定義の値で初期化するものの、書き込み前の読み取りは未定義動作
  6. 実装定義の値で初期化するものの、書き込み前の読み取りは誤った動作
    • 書き込み前の値の読み取りは誤っているものの、UBではない
    • コンパイラフラグなどによって、テストのために検出しやすい値で初期化したり、実運用のために安全な値で初期化したりする
    • あるいは、誤った動作を未定義動作として扱うこともできる
  7. 値初期化に一本化
    • 仕様からデフォルト初期化を削除する
    • これによって初期化は常に値初期化となり、仕様が単純化され、未初期化を含む初期化周りの問題が解決される

この提案では、これらを実現可能性、下位互換性、表現可能性の3つの観点から比較しています

  • 実現可能性 : そのソリューションが既存のC++標準に対して一貫しているかどうか。つまりは、C++標準に適用可能であるかどうか
    • 実現可能
    • 実現不可能
    • 不透明 : 現時点では判断できない
  • 下位互換性 : そのソリューションが採用された場合に、既存のコードを壊すことが無いかどうか。
    • 互換性がある : 以前にコンパイル可能なコードは引き続きコンパイル可能であり、UBの場合のみ動作が変更される
    • 正しいコードと互換性がある : 以前にコンパイル可能でUBを含まないものは引き続きコンパイル可能だが、UBを含むコードはコンパイルエラーとなる場合がある
    • 互換性がない : 以前に正しいコードもコンパイルが通らなくなる
    • 不透明 : 現時点では判断できない
  • 表現可能性 : そのソリューションが採用された場合に、既存コードの意味が変更されるかどうか。
    • 良い : 初期化を遅らせる意図を明示、あるいはロジックエラー(初期化忘れ)を修正するためにコードを更新する必要がある
    • 悪い : 意図的な初期化遅延とロジックエラー以外の可能性が発生することで、現在よりも状況が悪くなる
    • 変わらない : 意図的な初期化遅延もしくはロジックエラーを含むような(未初期化変数を含む)既存コードが曖昧ではなくなる
    • 不透明 : 現時点では判断できない

次の表は、先程の7つのソリューションに対してこれを比較したものです

ソリューション 実現可能性 下位互換性 表現可能性
1. 常にゼロ初期化 実現可能 互換性がある 悪い
2. ゼロ初期化/診断 不透明 正しいコードと互換性がある 変わらない
3. 初期化の強制 実現可能 互換性がない 良い
4. 遅延初期化を考慮した初期化の強制 実現可能 互換性がない 良い
5. 実装定義の値で初期化+その読み取りは未定義動作 実現不可能 互換性がある 変わらない
6. 実装定義の値で初期化+その読み取りは誤った動作 実現可能 互換性がある 変わらない
7. 値初期化に一本化 不透明 不透明 不透明

この表から、次のようなことが分かります

  1. 未初期化変数にまつわるセキュリティホールを塞ぐのに有効であるが、表現可能性が悪くなる
    • 現在の初期化されていないローカル変数は、初期化忘れ(ロジックエラー)か意図的な未初期化(遅延初期化のため)のどちらかですが、このソリューションの後での初期化されていない変数は、初期化忘れと意図的なゼロ初期化の区別がつかなくなるため
  2. ゼロ初期化とコンパイルエラーを組み合わせることは、コンパイラ間で拒否されるコードが変化しうることにつながる
  3. 意図的な遅延初期化のための注釈(ソリューション4)の有無にかかわらず、既存のコードを修正する多大な努力を強いることになる
    • ともすれば、未初期化変数を初期化するようにするスクリプトの安易な使用を招き、それによって元のコードの意図を見失う恐れがある
    • 既存のコードベースが抵抗する可能性がある
  4. 同上
  5. UBに定義された意味と動作を要求することは不可能
  6. 欠点が無く最も利得が高いが、誤った動作(erroneous behavior)という新しい概念を標準に導入する必要がある
    • 未初期化変数読み出しをEBとして指定することで、ソリューション7のような将来の変更をさたまげない
  7. より詳細な調査が必要

この提案は、あくまで代替案を含めたソリューションをまとめ比較するもので、どれを推しているわけでもありません。提案というよりも、議論を促すための文書です。

P2759R1 DG Opinion on Safety for ISO C++

WG21 Direction Group (DG)の安全性についての見解を説明する文書。

以前の記事を参照

このリビジョンでの変更は、寄せられたフィードバックを適用した事です。

P2763R1 layout_stride static extents default constructor fix

std::layout_strideのデフォルトコンストラクタの生成するレイアウトマッピングを修正する提案。

以前の記事を参照

このリビジョンでの変更は

  • LWGの要請により、全てのエクステントが静的であるかにかかわらずデフォルトコンストラクタが同じ振る舞いをするようにした
    • デフォルトコンストラクタは常に、std::layout_right::mappingと同じ方法でストライドを初期化する
  • 必要なスパンサイズがインデックス型で表現可能であることを保証するための事前条件の追加

などです。

この提案は、2月のIssaquah会議で全体投票をパスしてC++23に適用されています。

P2770R0 Stashing stashing iterators for proper flattening

stashing iteratorというイテレータカテゴリに属している標準ライブラリ内イテレータについて、そのカテゴリや振る舞いを修正する提案。

stashing iteratorとは、その間接参照がイテレータ自身の内部にあるもの(そのイテレータの生存期間と関連付けられているもの)を返すイテレータのことです。stashing iteratorinput_iteratorにしかならず、forward_iteratorではstashingは認められません。

stashing iteratorである標準ライブラリのイテレータにはstd::regex_iteratorstd::regex_token_iteratorがありますが、これらイテレータのカテゴリは現在Forward Iteratorになっており、カテゴリ指定が間違っています。

このイテレータカテゴリの間違いは、C++20のRangeアダプタで使用したときに深刻な問題を起こすことがあります。

#include <ranges>
#include <regex>
#include <iostream>

int main() {
  char const text[] = "Hello";
  std::regex regex{"[a-z]"};  // 小文字アルファベット1文字にマッチング

  // 範囲(sub_match)の範囲(match_results)
  std::ranges::subrange regex_range(std::cregex_iterator(
            std::ranges::begin(text),
            std::ranges::end(text),
            regex),
        std::cregex_iterator{}
      );

  // string_viewの範囲
  auto lower = regex_range
    | std::views::join  // sub_matchの範囲へと平坦化
    | std::views::transform([](auto const& sm) {
        // sub_matchオブジェクトが参照する文字列範囲をstring_viewへ変換
        return std::string_view(sm.first, sm.second);
    });

  // elloを1文字づつ改行して出力する(はず
  for (auto const& sv : lower) {
    std::cout << sv << '\n';
  }
}

このコードは一見すると問題なさそうに見えますが、実際にはダングリングイテレータが静かに生成されており、アドレスサニタイザーを使用しているとtransform_viewイテレータの最初の間接参照でheap-use-after-freeが発生します。

std::regex_iteratorはそのメンバとしてstd::match_resultsというサブマッチオブジェクトの範囲(std::vector<std::submatch>のようなもの)を所有しており、間接参照はそのメンバへの参照を返し、イテレータのインクリメントのたびにそのメンバは次のマッチ結果で上書きされます。

std::match_results自体も範囲であり、これはstd::submatchの配列のようなものです。そのため、上記のragex_rangeは範囲の範囲となっており、それをviews::joinに通すと平坦化によって直接std::submatchの範囲を得ることができます。

join_viewイテレータは、入力viewの外側イテレータstd::regex_iterator)と内側イテレータstd::match_resultsイテレータ)をコピーして保持しています。

// join_view::iteratorの概略
class iterator {
  // 入力範囲の外側範囲のイテレータ
  iterator_t<R> outer;

  // 入力範囲の内側範囲(*outer)のイテレータ
  iterator_t<range_reference_t<R>> inner;
};

// この場合、次のようになる
class iterator {
  // std::match_resultsオブジェクトを保持している
  std::regex_iterator outer;

  // std::match_results::iterator、例えばsubmatchのポインタ
  std::submatch* inner;
};

このようになっている時にjoin_viewイテレータをコピーすると、outer(及び内部のmatch_resultsオブジェクト)もコピーされますが、innerはコピー元のouterの要素を参照し続けます。コピー元イテレータのインクリメントや破棄によってそれが寿命を終えると、コピー後のjoin_viewイテレータはダングリングイテレータとなります。

コピーが起きなければ問題にならないのですが、残念ながら上記例ではtransform_viewイテレータ構築時にコピーが起きるとともにコピー元イテレータが即座に破棄されており、それによってダングリングイテレータが生成されてしまっています(とのことですが、正直どこでコピーが起きているのかわかりませんでした・・・)。

この問題はregex_iteratorがカテゴリについて嘘をついてるだけではなく、join_viewstashing iteratorを正しく扱えないことから起こっています。

この提案はこの問題の解決のために、それらに次のような変更を行います

  • regex_iteratorregex_token_iteratoriterator_conceptの変更
    • iterator_categoryの変更は破壊的かつ影響が大きいので行わない
  • join_viewjoin_with_viewinput_itetarorinput_range)に対して外側イテレータをそのviewオブジェクト内部にキャッシュするように変更
    • stashing iteratorを判定する方法が無いため、全てのinput_iteratorに対して適用

同時に、LWG Issue 3700(begin()が内側範囲に対してconstを考慮していない)とLWG Issue 3791(--内部で内側範囲が右辺値で得られる場合のハンドリング)の2つのjoin_view/join_with_viewにまつわる小さなイシューも解決しています。

この提案は既に2月のIssaquah会議で全体投票をパスしてC++23に採択されています。

P2771R0 Towards memory safety in C++

依存関係を追跡することによる時間的なメモリ安全性保証を言語に導入する提案。

時間的なメモリ安全性保証とは、破棄済みのオブジェクトへのアクセスや無効な参照の使用などのことで、例えば次のようなものです

int main() {
  std::string s1 = "abcdefg";
  std::string_view s2 = s1;

  std::cout << s2.length() << " " << s2 << std::endl;
  
  // 参照元を変更
  s1.erase(2,2);

  // s2の使用は危険
  std::cout << s2.length() << " " << s2 << std::endl;
}

対して、バッファオーバーランなど有効ではないメモリ領域へのアクセスは空間的なメモリ安全性の問題と言えます。空間的なメモリ安全性はランタイムのチェックや危険な機能(生ポインタやreinterpret_castなど)の使用禁止などの措置によって比較的簡単に防止することができます。

この提案の目的は、Rustのborrow checkerの導入(C++の現在のプログラミングモデルにそぐわない)やライフタイム注釈のような不十分なものの導入をしなくても、C++をメモリ安全にすることが可能であり、その議論を促すことです。

この提案のアプローチは、オブジェクトの依存関係を追跡することにより時間的なメモリ安全性を保証するものです。例えば、オブジェクトA(reference_wrapperなど)がオブジェクトBに依存するとき、Bが破棄された後にAを使用してはならないこと、あるいは、オブジェクトA(string_viewなど)がオブジェクトBのコンテンツに依存している時、Bの変更後にAを使用してはならないこと、を追跡し、それを静的に解析することで時間的なメモリ安全性を保証します。

この提案による安全性保証メカニズムは、何かしらの注釈によってスコープ単位で有効にするオプトインなもので、たとえば次のような構文によります

namespace oldcode {
  // 既存のC++コード
}

namespace newcode {
  [[memorysafety]];
  // このスコープのコードはメモリ安全性を強制する
}

namespace newcode {
  // 注釈がないので、メモリ安全性はチェックされない
  // newcodeとoldcodeから任意のものを参照できる
}

このmemorysafetyな領域内では、同じくmemorysafetyなコードを呼び出す限り、コンパイラは時間的なメモリ安全性を保証します。ただし非メモリ安全なコードを呼び出すとそのチェックは無効化されます。

memorysafetyな領域ではまず、メモリ安全性のために参照(ポインタ含む)のエイリアシングに制約を加えます。例えば次のような普通なコードはエイリアシングを考慮すると安全ではありません

void foo(vector<int>& a, const vector<int>& b) {
  if (!b.empty()) {
     // aとbが同じオブジェクトを指している場合危険(foo(x, x)のように
     a.push_back(b.front());
  }
}

memorysafetyな領域内ではデフォルトで安全にするために、あるオブジェクトの非const参照を関数に渡す際には他の参照はそのオブジェクトをエイリアスできなくします。この違反はコンパイラによってチェックされます。

// memorysafety内にあるとして
void bar(int x, int y) {
  std::vector<int> a, b;
  std::array<std::vector<int>, 2> c;

  foo(a,b); // ok
  foo(a,a); // error
  foo(c[x], c[y]); // error, x != yを証明できない

  auto& r1 = c[x];
  auto& r2 = c[y];
  if (&r1 != &r2) {
    foo(r1, r2); // safe now
  }
}

エイリアシングを許可したい場合は明示的にコンパイラに表明します。例えば

void swap(auto& a, [[mayalias(a)]] auto& b) { ... }

このswap()はオブジェクトに対して呼び出されても安全であるとみなされます。

多くのコードは引数がこのようなエイリアシングを起こしていると安全ではないため、エイリアシングを許可するのはオプトインである必要があります。

ただし、この制約は直接のエイリアスにのみ作用し、間接的なエイリアスの問題は依存関係の追跡によって対処されます。

void foo(vector<string>& a, const string& b) {
  a.push_back(b);
}

void bar(vector<string>& a) {
  // 直接のエイリアスではないものの、依存関係が発生している
  foo(a, a.front());
}

オブジェクトが破棄された後にその参照からのアクセスが発生してはなりません。参照がローカル変数のスコープの外側にある場合などはすべての用途について安全であると確信が持てない場合は参照はオブジェクトよりも長く存在してはならない。このチェックのために、オブジェクトと参照の生存期間を追跡する必要があります。

生存期間は依存関係としてモデル化され、グローバルなオブジェクトは(とりあえず)無限の寿命を持ち、ローカルなオブジェクトはそのスコープに応じた寿命を持ちます。ローカルオブジェクトが破棄された時、それに依存する全てのオブジェクトは

  1. トリビアルデストラクタを持つ
  2. そのオブジェクトを用いてメンバ関数呼び出しがなされない

必要があります。

void foo() {
  int* a;
  {
     int d = 2;
     a = &d; // aはdに依存
     *a = 3; // ok, dは生存期間内
  }
  // aはダングリング参照となるが、アクセスされなければ存在は許される

  // これはエラー、dは破棄済
  *a = 5;
}

関数呼び出し時に何かを渡す時、明示的な生存期間に関するアノテーションが必要になる場合があります。

void bar(int* x),

void foo(int& x, int* y) {
  int* a = &x;  // ok, aはxに依存
  bar(a);       // ok, xは生存期間内

  // error: yはxよりも長く生存しうる
  y = a;
}

ここでは、xが破棄された後でyが使用されないことを証明できないため、yへの参照の代入は禁止しなければなりません。許可するためには、依存関係を伝播するために注釈が必要になります

void foo(int& x, [[maycapture(x)]] int* y) {
  // ok, 呼び出し側はxとyの寿命をチェックできる
  y = &x;
}

メンバ関数の場合、thisに参照を保存する場合は同様の注釈を関数そのものに行います。また、戻り値で参照を返す場合も関数そのものに注釈が必要です。

[[maycapture(x)]] void Foo::bar(int* x) {
  this->y = x;
}

[[dependson(x,y)]] char* foo(char* x, char* y) {
  return x < y ? x : y;
}

これらの注釈によって破棄後のオブジェクトにアクセスしている場合を検出することができますが、最初のstring_viewの例のようにコンテンツの変更を検出することはできません。

このために、オブジェクトのコンテンツのキャプチャを明示する注釈を行います

[[dependson(*x)]] std::string_view foo(std::string& x) {
  return x;
}

これによって、次のようなルールを確立することができます

  • オブジェクトAがオブジェクトBのコンテンツに依存する場合、Bの非const関数が呼び出された後でAを使用してはならない

これは、オブジェクトの状態を変更するのは非const関数のみであり、あるオブジェクトの非const関数が呼ばれたらそのオブジェクトに依存する全てのオブジェクトは無効になる、という単純なルールです。しかし実際には、非constでありながらオブジェクトの状態を変更しない関数が存在してるためこのルールは少し厳しいものです。例えば、コンテナのbegin()/end()などがあり、明らかにbegin()/end()の呼び出しでその時点で取得されているイテレータを無効にしたくはありません。

そのため、ここでもそれらの関数がnon-mutatingであることをマークする注釈が必要になります。これによって、コンテンツに依存するオブジェクトはそのような非const関数の呼び出しの後でも有効なままでいることができるようになり、コンパイラはそのような関数が他の注釈なしの非const関数を呼び出さないように強制する必要があります。

template <class T>
[[dependson(*this), nonmutating]] myvec<T>::iterator myvec<T>::begin() {
  return iterator(this->ptr);
}

この上で、次のルールを確立することで、a.push_back(a.front())のような例を検出することができます

  • あるオブジェクトによる関数呼び出しの引数は、そのオブジェクトに対する他の非const関数引数に依存してはならない

a.front()aのコンテンツに依存しているので、a.push_back()に渡すことはできません。

ここまでの形式はほとんどのユースケースに対応していますが、標準ライブラリでも使用されている用例で禁止されているものが1つあります。例えばstd::vector::insert()の次の様な呼び出しです

a.insert(a.end(), b.begin(), b.end());

ここまでのルールによって、b.begin()b.end()aに依存してはならないことが要求され、それは正当なものです。しかし、isnert()の最初の引数は異なり、依存関係を受け入れる必要があるほかイテレータが同じコンテナのものであることを要求しています。

多くの場合、これを静的に証明することはできず、insert()に伴うイテレータ無効化によって実装自体も厄介です。ここでは、insert()の実装には安全ではないコード(およびイテレータがそのコンテナのものであることを検証するアサート)が必要になることを受け入れ、依存関係を許可するためのアノテーションを導入するにとどめています。

template<class InputIt>
[[dependson(*this)]] iterator insert([[maydependon(this)]] const_iterator pos, InputIt first, InputIt last);

ここまでのルールによって、多くの時間的なメモリ安全性を壊すコードを検出することができるようになります。しかし、コンパイル時にすべてのバグを検出するには十分ではありません。エイリアスによって、まだ時間的な安全性を損ねる可能性が残されています

void f1() {
  A a;
  B b;
  C c;
  a.push_back(123);
  f2(a, b, c);

  f3(b);  // b.iがaから取得される
  f4(c);  // b.iが無効化される
  f5(b);  // b.iが使用される
}
void f2(A& a, [[maycapture(a)]] B& b, [[maycapture(a)]] C& c) {
  b.a=&a;
  c.a=&a;
}
void f3(B& b) {
  b.i = b.a->begin(); // bにaのイテレータを保存
}
void f4(C& c) {
  c.a->clear(); // c経由でaのイテレータを無効化
}
void f5(B& b) {
  b.e = *b.i; // aのイテレータの使用
}

生存期間の制約は満たされていますが、aエイリアスとそれを介した操作によってダングリングイテレータが発生しています。これらの関数が別々の翻訳単位に定義されている場合、これを検出することはできません。この挙動を検出するために非常に精巧なアノテーションを導入することもできるかもしれませんが、それはあまり現実的ではないようです。

その代わりに、この問題は実行時のチェック(サニタイザーのようなもの)によって検出することにしており、このチェックは例えばデバッグ時のみとすることができるようにすることを想定しています。

これらのようにして、コンパイル時になるべく多くの問題を発見しつつ、それが難しい部分は実行時チェックに任せることで、時間的なメモリ安全性を保証します。

P2772R0 std::integral_constant literals do not suffice - constexpr_t?

コンパイル時定数オブジェクトを生成するクラスとリテラルの提案。

この提案は、P2725のstd::integral_constantリテラルの提案を受けてのもので、P2725のソリューションではカバーしきれないユーズケースに対応可能なようにP2725の提案を拡張するものです。P2725に関しては以前の記事を参照

P2725がカバーできていないユーズケースとは、次のように、非整数のNTTPをconstexpr引数として関数に渡したい場合です

// P2725のintegral_constantリテラルに対応する変数テンプレート
template<auto N>
inline constexpr std::integral_constant<decltype(N), N> Const = {};

template<typename T>
struct my_complex {
  T re , im;
};

template<typename T>
struct X {
  void f(auto c) {
    // cからNTTP値を引き出して定数式で使用可能
  }
};

inline constexpr short foo = 2;

template<typename T>
void g(X<T> x) {
  x.f(Const<1>);
  x.f(Const<2uz>);
  x.f(Const<3.0>);
  x.f(Const<4.f>);

  // P2725の提案ではこれらができない
  x.f(Const<foo>);
  x.f(Const<my_complex(1.f,1.f)>);
}

この例の最後の関数g()内部での最初の4つのx.f()呼び出しはP2725のstd::integral_constantリテラル1icなど)でも可能ですが、最後の2つの呼び出しはサポートされていません。P2725はあくまで整数定数のNTTPを同様の形で渡すためのリテラルを提案しているだけで、より一般的なNTTP(特に、C++20で許可されたクラス型)を渡すことができません。

関数にcosntexpr引数を渡したいケースは多くC++20以降は特にそれは整数型に限りません。従って、P2725が解決を目指している問題空間はより広いものであり、P2725の内容だけでは不完全です。また、このようなNTTP渡しにおいてはより簡単に行えることが望ましく、1ic(P2725のリテラル)とstd::cnst<1>(上記例のConst)の両方が必要です。

また、上記例では整数以外のものもstd::integral_constantに渡していますが、これは現在の定義でも行うことができます。その場合、このソリューションのためにintegral_constantを引き続き使用すべきではなく、そのための新しい型が必要になります。提案では、次のような型を例示しています

template<auto Value>
struct constexpr_t {
  using value_type = decltype(Value);
  using type = constexpr_t;

  static inline constexpr value_type value = Value;
  
  constexpr operator value_type() const noexcept { return Value; }
  static constexpr value_type operator()() noexcept { return Value; }
};

新しい型を追加すれば、P2725で問題となっている単項-std::integral_constantに追加する破壊的変更についても解決され、また一貫性のために他の演算子も追加することができます。

この提案ではさらに、これらの機能によって可能となるAPIについて記載しています。たとえば

  • std::arrayや固定サイズstd::span(及び提案中のstd::simd)などのsize()メンバ関数をこの定数型(std::integral_constant/constexpr_t)に置き換える
    • これらの型がoperator()を持つことによって、APIレベルでは破壊的変更にならない
    • メンバポインタを取られている場合のみ破壊的となるが、それは禁止されている
  • 非メンバoperator[]によって、contiguousな範囲からスライスを取得するAPI

この提案はまだ問題提起に留まっていますが、LEWGのレビューではこの提案の方向性が支持されているようで、この提案を勘案しながらP2725のソリューションを検討していくようです。

P2773R0 Considerations for Unicode algorithms

P2728/P2729で提案されているユニコードアルゴリズム(変換・正規化)について、推奨事項等のフィードバックの提案。

P2728/P2729については以前の記事を参照

この提案は、P2728/P2729の提案するユニコードアルゴリズム(文字列範囲に対するアルゴリズム)について、筆者の方の実装経験や考えをもとに推奨事項を記述するものです。

この提案の推奨事項の要旨は次のようなものです

  • ほとんどのユニコードアルゴリズムviewとして公開する必要がある
    • <ranges>の要件を満たす
  • ユニコードアルゴリズムは、標準の他のviewアルゴリズムとうまく構成できる必要がある
  • 正規化・クラスタ化・ケーシング(大文字小文字変換)を最初の作業の焦点とするべき
    • UTF相互変換を前提とする(それがなければ何もできない)
  • ユニコードアルゴリズムはコードポイント(ユニコード空間上の32bit整数値)で動作する
  • 調整済と未調整(Tailored and non-tailored)のアルゴリズムではそれぞれ異なる要件と実装上の課題があるため、類似しつつも別々のインターフェースで公開されるべき
  • 調整済アルゴリズムに取り組む前に、ロケール表現についてよく理解しておく必要がある
  • Rangeアダプタオブジェクト(|構文)は暗黙的なUTFデコード/エンコード手順を導入するのに最も適した場所。このような暗黙の手順はユーザビリティのために必要
  • char32_tはコードポイントを表現するのに適切な型である
  • charstd::byteの消費も可能だが、明示的であるべき
  • コードユニット(UTFエンコーディングの1単位の整数値)のシーケンスはデフォルトで検証すべき
  • ICUを使用した実装を可能とするために、未調整アルゴリズムを制約すべきではない
  • ICU4xは調整済アルゴリズムに対する長期的な最善の答えである
  • 将来のユニコードバージョンで変更されうる仮定を公開するのは避けるべき
  • プロパティルックアップを最適化し非sized_rangeのメモリを巧妙に確保することで、既存のフレームワークと遜色ないパフォーマンスを実現できる
  • ユニコードアルゴリズムは、インプレースの変換や文字列コンテナの恩恵を受けられない
  • UTFのデコード/エンコードを回避しても、明確にパフォーマンスが向上するとは限らない

この提案はP2728/P2729の内容を否定したり批判したりするものではなく、ユーザビリティユニコード固有事情などの面からC++の標準ライブラリとして望ましい方向性を示すものです。

P2779R0 Make basic_string_view's range construction conditionally explicit

std::string_viewrangeコンストラクタのexplicitを条件付きに緩和する提案。

std::string_viewstd::basic_string_view)に対する文字の範囲から構築するrangeコンストラクタは、C++23で追加されており、そのコンストラクタはexplicit指定されています。その目的は、文字の範囲を常に文字列として変換することには問題があるためで、文字の範囲からの構築時にその意図を確認するために明示的なものとするためです。

なお、std:stringstd::string_viewへの暗黙変換演算子を備えているため、このコンストラクタと関係なくstd::string_viewへ暗黙変換できます。

しかしこのことによって、独自定義された文字列型をstd::string_viewへ暗黙変換することも禁止されています。

void use(std::string_view v);

std::string str1;
use(str1);  // OK

// 自分で作成したものだったり、どこかのライブラリのものだったり
my::string str2;
use(str2);  // ERROR

このような在野のstring(あるいは同様のstring_view)型は必ずしも自分が定義したものではなく、別のライブラリに属するものかもしれません。その場合、自分で暗黙変換を提供することもできません。また、この問題は逆に、独自定義のstring_viewをライブラリのインターフェースとしているようなライブラリにおいて、std::stringで問題になる可能性もあります。

void lib::very_useful_algorithm(lib::string_view v);

std::string str;
lib::very_useful_algorithm(str);  // ERROR

これらのコードのコンパイルが妨げられる理由はなく、この提案は、現在のrangeコンストラクタのexplicitにこれらの文字列型を検出する条件を指定することでこの問題を解決しようとするものです。

その際問題となるのは、どのようにしてそれら在野の文字列型を識別するかという点です。

しかし残念なことに、std::stringstd::vector<char>が名前以外ほとんど同じクラスであるように、在野の文字列型を識別することは困難です。そのため、必然的に何かしらの方法でオプトインする手段が必要となります。そのために、次の2つが提案されています

  1. std::ranges::enable_view<T>またはstd::ranges::view_baseのように、特殊化/継承して有効化する特性の導入
  2. 文字列型に共通する、何かしらのユニークな特性を利用する
    • 入れ子traits_typeを活用する(P2499で以前に提案されていたが採用されなかった)

どちらにも

  • どちらのオプションでも既存コードからのオプトインのための作業が必要となる。
    • ただし、オプション1を実装するコードは存在していないが、オプション2は既に実装しているものが存在する
  • traits_typeは必ずしも文字列型に固有のものではなく、全く異なる意味で同じ名前を使用している可能性がある
  • オプション1は、それが適用され利用可能になるまでに時間がかかり過ぎる
    • それが採択され、実装されて利用できるようになるまでに1つのC++リリースサイクルの間問題が解決しない

オプション2の欠点については、std::string_viewrangeコンストラクタは現在でも制約によって厳密に文字の範囲を判定しており、その判定をパスしたうえでtraits_typeを持つようなクラスというのは実際にはほぼ存在しえないと思われます。この提案では在野のライブラリを調査することで、traits_typeを持たない文字列型はあっても、traits_type持ちながら(std::string_viewrangeコンストラクタの制約をパスして)文字列型であると認識される型は見つからなかったようです。

この提案はオプション2を推しており、それをC++23へのDRとすることを提案しています。

P2782R0 A proposal for a type trait to detect if value initialization can be achieved by zero-filling

値初期化をゼロフィルに置き換えることが安全な型を検出するための型特性を追加する提案。

値初期化とは型の値を初期化するときにゼロ相当の値で初期化することです。値初期化は初期化対象の領域にゼロ相当の値を代入しなければならないため、std::uninitialized_value_constructアルゴリズムに見られるように、たくさんの要素を値初期化する場合にパフォーマンスが低下する可能性があります。

// std::uninitialized_value_construct()の実装例
template<class ForwardIt>
void uninitialized_value_construct(ForwardIt first, ForwardIt last) {
  using Value = typename std::iterator_traits<ForwardIt>::value_type;
  ForwardIt current = first;
  
  try {
    // 範囲[first, last)の要素を1つづつ値初期化
    for (; current != last; ++current)
        ::new std::addressof(*current) Value();
  } catch (...) {
    std::destroy(first, current);
    throw;
  }
}

初期化対象領域が連続している(contiguousである)時で、初期化対象の型(Value)が単純なデータ型の場合、この初期化ループはmemset(ptr, 0, bytes_size)のようなコードで置き換えることができます。これは初期化する領域サイズが大きければかなりのパフォーマンス向上につながることがあり、コンパイラはこのようなコードに対してそのような最適化を行うことがあります。

単純なデータ型とは例えばint型のような数値型の事ですが、クラス型の場合はメンバやコンストラクタの宣言の仕方などによって変化するため簡単には判断できません。また、組み込み型でも浮動小数点数型やポインタ型等のそのオブジェクト表現が標準で義務付けられていない型の場合も(ゼロ相当の値がゼロフィルによって生成される値と同等であるかが分からないため)、単純なゼロフィルによって初期化することが適切であるかは分かりづらいところがあります。それらの型がゼロフィルで値初期化可能であるかどうかは最終的には実装あるいはプラットフォームによって決まります。

そのため、コンパイラはこのような最適化が可能かどうかを適切に判断することができ、可能な場合には値初期化ループをゼロフィルに置き換えるコードを出力してくれるのですが、次のような理由によってコンパイラオプティマイザを信頼するには問題があります

  • コンパイラは時々最適化できるはずのコードを見逃すことがある
  • 最適化を有効にする必要がある(GCCなら-O2以上など)
  • 最適化によってデバッグ効率が低下する
    • 最適化を無効にすると、非効率的なコードを生成する
  • 最適化はコンパイル時間増大につながる

これらの理由から、標準ライブラリを含めた多くのライブラリ実装がそのような最適化を手動で実装しています。つまり、先ほどのuninitialized_value_constructの実装のようなコードでは、初期化対象の型がゼロフィルによって安全に初期化することができる(値初期化と同等になる)ことが検出できた場合にのみ、memsetを用いたコードへディスパッチするようにしています。

特に、そのようなコードはBoost.ContainerやFolly、Qtなどのライブラリで見ることができますが、現在のその判定は何かしらの間違いがあるようです。値初期化をゼロフィルに置き換えることが安全ではない型の値初期化をゼロフィルで行ってしまうと、ともすれば深刻で見つかりづらいバグにつながる可能性があります。

この提案は、そのような型の検出を行う型特性を追加することで、それが正しくかつ完全に検出可能となるようにしようとするものです。それによって、標準ライブラリ以外のライブラリにおけるこの手の最適化コードの正確さを向上させることができ、潜在的なバグを削減することができます。

提案されているのは、std::is_trivially_value_initializable_by_zero_filling<T>という型特性で、これはTの値で初期化をゼロフィルに置き換えることが安全である場合にtrueとなり(true_typeから派生し)ます。

// 宣言例
namespace std {
  // 型特性の本体
  template<class T>
  struct is_trivially_value_initializable_by_zero_filling;

  // 簡易アクセス用変数テンプレート
  template<class T>
  constexpr bool is_trivially_value_initializable_by_zero_filling_v
    = is_trivially_value_initializable_by_zero_filling<T>::value;
}

これをtrueにするTtrivially value-initializabile by zero-fillingとよばれる型で、次のいずれかに該当する型として指定されます

  • 整数型
  • 列挙型
  • そのほかのスカラ型で、trivially value-initializabile by zero-fillingである実装定義の型
  • trivially value-initializabile by zero-fillingな型の配列型
  • trivially value-initializabile by zero-fillingなクラス型
    • 資格のある(eligibleな)トリビアルデフォルトコンストラクタを持ち、かつ
    • 全ての非静的メンバおよび基底クラスは、trivially value-initializabile by zero-fillingな型

前述のように、浮動小数点数型やポインタ型などはその値初期化される値をゼロフィルによって生成可能であるかが実装定義であるため、この型特性はコンパイラマジックで実装される事になるでしょう。また、同様の理由によりその結果は対称のプラットフォームによって変化する可能性があります。

P2784R0 Not halting the program after detected contract violation

契約違反が発生した際にすぐに停止せずにプログラムの実行を継続する機能についての提案。

現在進行中の契約プログラミング機能においては、契約違反が起きた場合(契約条件がfalseを返した場合)にその時点でプログラムを停止させ、他の選択肢を提供していません。契約違反が起きたと言うことはその指定された意図から外れて実行されており、そのまま継続したとしても結果は予測できないものになります(クラッシュや未定義動作など)。このデフォルトは厳しいですが適切なもので、契約違反時の継続モードについてはC++20契約の際にも問題となったことの一つでもあります。

しかし、実際には必ずしもそのデフォルトが最適とは言えない場合があります。たとえば

  1. プログラム内の分離されたサブコンポーネントの1つで契約違反が起きた時でも、他の部分に影響を与えないことが確信できる場合
  2. 上記の特殊なケースとしてmain()を1つのサブモジュールと見做した時、1回目のmain()実行における契約違反が2回目のmain()実行の正しさに影響しないことは、どうにかしてそのプログラムを再起動させたときにわかる可能性がある
  3. 単体テストの場合、契約チェックのような安全策の存在を確認するために意図的に契約違反を起こす場合がある
  4. プログラムが本番環境で長い間使用されており、その制御パスのほぼ全てを使用している可能性が高い場合、プログラムは内部仕様(を表現した契約)に固執しなくてもユーザーの期待通り動作していると確信できる場合

最後の例はともかく初めの3つの場合には、プログラムの実行を止めたくないが、契約違反が検出された場所とは異なる場所からプログラムの実行を再開することができ、それが望ましくすらある、と言う点が共通しています。この提案はこのユースケースに焦点を当てて、これを満足させるために取れるソリューションについて検討するものです。

ユースケースは少し異なりますが、ほぼ同様のことはP2698R0でも提案されており、そこではその方法として例外を用いるEval_and_throwモードを提案しています。ただし、これについてこの提案では次のような問題点や疑問点を提示しています

  1. 例外送出によってスタック巻き戻しが発生しデストラクタが呼ばれるが、デストラクタもまた契約を持つ可能性があり、同様に契約が破られうる
    • スタック巻き戻し中のデストラクタで例外が発生すると、結局std::terminate()される
  2. 契約(特に事前事後条件)の存在とnoexceptについてが未解決
    • 例外を投げうる契約指定に対してnoexceptはどう言う意味を持つのか、あるいは持たせるのか?
  3. 例外を無効にしてコンパイルされているプログラムにおいて、契約違反後の継続モードを提供する方法がない
    • 例外を無効にしているプログラムは少なくはないが、そのようなプログラムにおいても同じ要求があるはず

この提案の契約違反後の継続モードは、スタック巻き戻しよりも厳しくstd::abort()よりも柔軟な機構を提案しています。それは、2つの標準ライブラリ関数から構成されます

// コンポーネント境界を指定する関数
template <invokable F>
void abortable_component(F&& f);

// プログラムを終了させずに、コンポーネントを終了させる関数
[[noreturn]]
void abort_component() noexcept;

abortable_component()は、渡された関数fをほとんど通常通りに実行しますが、その実行は別のコンポーネントで実行されているものとして扱われます。abortable_component()は例外中立であり、fの呼び出し中の全ての例外はここから送出されます。この関数は、プログラマが想定するコンポーネント境界をコンパイラに指示するためのものです。

abort_component()を呼び出すと、abortable_component()によって指定されたコンポーネント境界に到達するまで、スタック巻き戻し(それに伴うデストラクタ呼び出し)を伴わずに現在のコールスタックから離脱する処理が開始されます。その処理が完了すると到達したコンポーネント境界の直後、すなわちabort_component()の呼び出しが発生したabortable_component()呼び出しの直後の地点からプログラムの実行が再開されます。もしこのとき、対応するコンポーネント境界が見つからない(abortable_component()の内部ではない)場合は、単にstd::abort()が呼ばれます(これは、違反後即終了と同じ動作)。

すなわちこれらのものは、例外送出に伴う大域脱出を行いつつもスタック巻き戻しに伴うデストラクタ呼び出しや例外オブジェクトのコピーを回避し(それによってネストする契約違反の発生を回避し)、なおかつ大域脱出は指定されたコンポーネント境界で止まる、と言うことを実現するものです。

提案より、サンプルコード

struct Guard {
  ~Guard() { std::printf("A"); }
};

int fun() {
  Guard g;
  std::abort_component();          // (2) 中断シーケンスの開始
  std::printf("B");                // (3) この行はスキップ。"B"は出力されない
}                                  // (4) デストラクタ呼び出しもスキップ。"A"は出力されない
 
int main() {
  std::abortable_component(&fun);  // (1) fun()をサブコンポーネントとして実行
  std::printf("C");                // (5) サブコンポーネントから離脱すると、"C"が出力される
}

これらの機構はどこでコンポーネントが中断したかの情報やコンポーネントが正しく完了したかについての情報を伝達する方法を持たないほか、abort_component()呼び出しによるコンポーネントの中断はデストラクタの実行を伴わないことから簡単にリソースリークを発生させ、プログラムの継続を危うくします。

とはいえこれは、契約違反が起きた場合を前提とした、既にプログラムの継続が危うい状況における被害を最小限に抑えるためのツールであり、その場合にやるべきことはプログラムを正しく動作させることではなく、契約違反の原因(おそらくバグ)の影響を最小限に抑える策をとることです。これは危険な機能であり、そのような状況以外で使用することは推奨されません。abortable_component()をプログラム中に配置することは、それによって呼び出された関数の任意の部分(RAIIも含めて)がスキップされた時でも(あるいはその処理の成否に関わらず)プログラムの実行を継続することが合理的に安全であると、プログラマが判断したことを意味します。

この機構は、現在の契約機能(MVP)に専用のビルドモードを追加する必要がなく、現在のEval_and_abortモードで契約条件違反が検出された際に呼び出されるstd::abort()abort_component()に置き換えるだけでサポートできます(abortable_component()の呼び出し内部でなければ、その呼び出しはstd::abort()と等価なため)。

この場合abort_component()は通常使用可能である関数である必要がない(任意の使用を抑制できる)ほか、MVPの正式な策定(C++26予定)の後から後方互換性を保ちながら導入することもできます。

P2786R0 Trivial relocatability options

trivially relocatableをサポートするための提案。

trivially relocatableとは、memcpyあるいはビット毎コピーによって再配置(relocation)することができる型の性質のことで、relocationとはムーブとムーブ元オブジェクトのデストラクタ呼び出しが複合したかのような操作のことです。この提案は、P1144などで以前に提案されていた同様の性質についてを筆者の方の実装経験をもとに変更・改善を加えるものです。

P1144及び再配置操作については以前の記事を参照

この提案のトリビアルな再配置操作(trivial relocation operation)とは、ソースオブジェクトのストレージが別のオブジェクトによって使用されたかのようにそのソースオブジェクトの生存期間を終了するようなビットコピー操作です。これはムーブ構築の直後にソースオブジェクトを破棄するのと意味的には等価ですが、実際にはソースオブジェクトに対して何も行わず、デストラクタは実行されません(例外は許可されています)。

trivially copyableな型はそれが可能なのはすぐに想像がつきますが、実際には非トリビアルなムーブコンストラクタ/デストラクタを持つような型であっても可能なものがあり、std::vectorstd::unique_ptrなどの多くのリソース所有型が該当します。トリビアルな再配置操作では、ソースオブジェクトのデストラクタ呼び出しをスキップすることでムーブ先オブジェクトがムーブコンストラクタで行うべきムーブ後操作をスキップすることができます。

現在のC++の仕様においては、trivially copyableではない型でこのようなビットコピーによるムーブは未定義動作となり、再配置操作を行うことができません。

この提案の目的はこのサポートを行うことにあり、次の3つを目標としています

  1. トリビアルな再配置操作をサポートするために、標準でそれを明確に定義する
  2. より多くのtrivially relocatableな型を暗黙的にサポートする
  3. トリビアルな再配置が誤用された場合によりより診断メッセージを出力すること

この提案のP1144との違いは

  • トリビアル性は構文ではなく意味から決まる
    • P1144は再配置可能という性質をアクセス可能なデストラクタとムーブコンストラクタから構文的に構成され、そのトリビアル性も同様
    • この提案の再配置可能性ではトリビアル性が重要であり、言語の他の部分と同様に、ある型のトリビアル性はその全てのメンバと規定クラスのトリビアル性から決定される
  • 実装品質に作用されない、予測可能な再配置可能性の指定
    • P1144は再配置操作のためにビットコピーを使用すること(トリビアルな再配置操作)を使用する許可を与えるが強制せず、それを使用することは実装品質の問題としている
    • また、再配置に適していない型に対する注釈をつける誤用は、UBや診断不要のill-formedとなる
    • この提案では、再配置操作に関わるセマンティクスを完全に指定するとともに、機能が誤用された場合に診断可能なill-formedとする
  • std::swap()を3つの再配置操作によって実装可能か?
    • P1144では、swap操作を3つの再配置操作によって実装可能な型のみをサポートするように制限されているが、これによってpmrコンテナをサポートできなくなっている
    • この提案では、そのような制約を要求しない
  • pmr型のサポート
    • この提案では、std::pmrの型に代表されるスコープアロケータモデルをサポートすることを強い動機としている
    • P1144では、通常のアロケータモデルしか考慮していない様子

この提案では、relocatetrivially relocatableと言った性質の意味を定義した上で、非trivially copyableなクラス型に対してそれを明示的に指定するために、trivially_relocatable(expr)という指定子を導入します。

struct Relocatable trivially_relocatable(true ) {}; // trivially relocatable
struct Alternative trivially_relocatable(false) {}; // not trivially relocatable

そして、トリビアルな再配置操作を行うライブラリ関数を導入します

template <class T>
  requires is_trivially_relocatable_v<T>
T* trivially_relocate(T* begin, T* end, T* new_location) noexcept;

この関数は、単なるmemcpymemmoveトリビアルな再配置操作を意図しているかをコンパイラが判断できないため、その意図をコンパイラに伝えることを目的としています。効果は、単にmemmoveを行うだけです。trivially_relocate(&src, &src + 1, &dst)srcオブジェクトの生存期間が終了しdstオブジェクトの生存期間が開始されたことを表し、コンパイラ等のツールはそれを認識することができます。

使用されているis_trivially_relocatable_vは型のtrivially relocatable性を検出するための型特性です。

提案より、診断の例

struct MyType trivially_relocatable : BaseType {
  
  // ユーザー定義ムーブコンストラクタの存在によってこの型は*trivially relocatable*ではないが
  // trivially_relocatable注釈によって*trivially relocatable*であることを指定する
  MyType(MyType&&); 
};

struct NotRelocatable : BaseType {
  
  // ユーザー定義ムーブコンストラクタの存在によってこの型は*trivially relocatable*ではない
  NotRelocatable(NotRelocatable&&);
};

struct Error trivially_relocatable : BaseType {
  NotRelocatable member;
  
  // trivially_relocatableと注釈されているが、*trivially relocatable*ではない型をメンバとして持つ
  // そのため、ill-formed。コンパイルエラーとなる
  // trivial relocationの最中にはムーブコンストラクタは呼び出されないため、この定義がそれに抗うことはできない
  Error(Error&&);
};

P2787R0 pmr::generator - Promise Types are not Values

P2787R1 pmr::generator - Promise Types are not Values

std::generatorpolymorphic_allocatorを使用するエイリアスpmr::generatorを追加する提案。

std::generatorは他のアロケータ対応(allocator aware)なコンテナ型等と異なり、生成する要素のためにアロケータを使用するのではなく、コルーチンフレームを保存しておく領域をコルーチンの初期化時に確保するためにアロケータを使用します。そのため、std::generatorは生成要素へのアロケータ伝播を行わず、それはstd::generatorの役割ではありません。

しかし、アロケータのカスタマイズは標準ライブラリの他の型と同じようにテンプレートパラメータで行い、使用可能なアロケータに関しても差異はありません(アロケータオブジェクトの渡し方はコルーチンの事情により少し異なりますが)。したがって、polymorphic_allocatormemory_resourceによってアロケータをカスタマイズすることもでき、std::generatorに対してそうしようとするのは自然な発想です。

しかし、std::generatorには3つのテンプレートパラメータがありアロケータは一番最後のパラメータによってカスタマイズしますが、その場合は3つのテンプレートパラメータを全て明示的に指定しなければならず、使いにくくなります。

std::pmr::monotonic_buffer_resource mbr;
std::pmr::polymorphic_allocator<> pa{&mbr};

// 型名が長くなる
std::generator<int, void, std::pmr::polymorphic_allocator<>> g = pmr_requiring_coroutine(std::allocator_arg, pa);

template<typename T>
using pmr_genterator = std::generator<T, void, std::pmr::polymorphic_allocator<>>;

// エイリアスがあれば使用感がかなり良くなる
pmr_genterator<int> g2 = pmr_requiring_coroutine(std::allocator_arg, pa);

std::pmr名前空間の意図は、アロケータをカスタマイズ可能な型についてpolymorphic_allocatorをデフォルトとした型名を提供するもので、これまではそういう型しかなかったとはいえコンテナ型などのアロケータ対応型のためだけのものではないはずです。また、std::generatorpolymorphic_allocatorを使用する場合は上記のようなエイリアスを作成することになるはずです。

この提案は、本来不必要な作業を強いることを回避するために、標準でstd::pmr::generatorエイリアスを用意しておくべきという提案です。

// 追加する宣言例
namespace std {

  // std::generator本体
  template<class Ref, class V = void, class Allocator = void>
  class generator;

  namespace pmr {
    // pmr::generatorエイリアス
    template<class R, class V = void>
    using generator = std::generator<R, V, polymorphic_allocator<>>;
  }
}

これによって、std::generatorとほぼ同じ使用感によって、polymorphic_allocatorを使用したgeneratorを使用できるようになります

std::pmr::monotonic_buffer_resource mbr;
std::pmr::polymorphic_allocator<> pa{&mbr};

// この提案後
std::pmr::generator<int> g = pmr_requiring_coroutine(std::allocator_arg, pa);

この提案は、すでに2月のIssaquah会議でC++23向けに採択されています。

P2788R0 Linkage for modular constants

名前付きモジュール内で定義されているconst変数のリンケージを通常の変数と同様に決定するようにする提案。

Cでは定数を宣言する場合にマクロが良く使われますが、C++ではその代わりにconst変数を使用することができます。

// Cでの定数
#define MAX_BUNNIES 57
struct bunny bunnies[MAX_BUNNIES];

// C++での定数
const int max_bunnies=57;
bunny bunnies[max_bunnies];

ただし、C++においてこのmax_bunniesのような定数がヘッダで宣言される場合、その定義は複数の翻訳単位に現れる可能性があります。この場合に多重定義の問題を回避するために、C++ではこのような変数に暗黙的に内部リンケージを与えています。ただし、ODR違反を起こす可能性が完全に排除されたわけではありません。

C++17では、inline変数によってこのような定数をinline constexprとすることでODR的に完全に安全な定数を宣言できるようになりました。

また、C++20モジュールでは、inlineはそのリンケージを変更する程度の意味しか持たず、inlineであったとしても定義は翻訳単位全体で1つである必要があるため、同様にODRの問題が解消されます(exportやモジュールリンケージによって参照することで、定義を各翻訳単位に用意する必要が無くなる)。

しかし、const変数に暗黙に内部リンケージを与える特別扱いはモジュールにおいても残っており、これによってモジュール内部からのそのような変数の参照が不可解な問題を起こします。

/// 翻訳単位 #1 モジュールAの実装パーティション
module A:B;

// 内部リンケージ
const int dimensions = 3;

/// 翻訳単位 #2 モジュールAの実装単位
module A;

import std;
import :B;

using vector = std::array<double, dimensions>;  // error: dimensionsは内部リンケージ

dimensionsexportを付加すると外部リンケージが与えられるため、この問題は同じモジュールの内側に閉じています。

また、このような変数は非内部リンケージの関数から使用することは禁止されています

/// 翻訳単位 #1 モジュールAのプライマリインターフェース単位
export module A;

// 内部リンケージ
const double delta = 0.01;

// 外部リンケージ
template<class F>
export double derivative(F&& f, double x) {
  return (f(x+delta)-f(x))/delta; // インスタンス化するまでエラーにならない
}

/// 翻訳単位 #2 モジュールAの実装単位
import A;

double d = derivative([](double x) {return x*x;},2);  // error: derivative()の定義は内部リンケージ名deltaを参照している

モジュールにおいては、明示的にinline, extern, exportのいずれかを付加することでリンケージを変更することができますが、これらはそれぞれ少しずつ意味が異なるため、どれを使用するべきかは微妙です

  1. inlineは(標準的には)リンケージの変更のみを行う
  2. externは、定義に適用する必要があり、モジュールリンケージを与える
  3. exportは、異なる状況でexportを取り除いた時だとモジュール内部では影響がない
    • 外部リンケージからモジュールリンケージになるだけのはずでは・・・

これらのいずれかの指定を明示的に変更しなければならないということは、非inlineconst変数を使用している従来のヘッダーファイルベースのライブラリをモジュールに移行する際の障害となります。

この提案ではこの問題を根本的に解決するために、インポート可能なモジュール本文内では名前空間スコープのconst変数に暗黙的に内部リンケージを与える仕様を無効化し、通常の変数と同じ方法でリンケージを決定するようにします。これによって、exportが付加されない場合は通常これらの変数はモジュールリンケージが与えられるはずです。

ただし、インポート可能なモジュール本文内とあるように、モジュール実装単位やグローバルモジュール(特にヘッダユニット)ではその扱いは従来と同じとなります。グローバルモジュールは破壊的変更になるため当然として、モジュール実装単位は他の翻訳単位からインポートされることが無くこの変更を適用する意味がないためです。

この提案はC++20へのDRとすることを提案しています。

この提案はNBコメントの解決であることもあり、すでに2月のIssaquah会議でC++20のDRとして採択されています。

P2789R0 C++ Standard Library Ready Issues to be moved in Issaquah, Feb. 2023

2月に行われたIssaquah会議でWDに適用されたライブラリに対するIssue報告の一覧

P2790R0 C++ Standard Library Immediate Issues to be moved in Issaquah, Feb. 2023

2月に行われたIssaquah会議でWDに適用されたライブラリに対するIssue報告の一覧。こちらはC++23で新規追加されたライブラリ機能に対するものか、NBコメントを受けてのIssue報告です。

P2791R0 mandate concepts for new features

新しいライブラリ機能がテンプレートを使用する場合、その制約をコンセプトによって指定するようにする提案。

C++20でコンセプトが導入されて以降もいくつかの新しいライブラリ機能が標準に導入されていますが、そのテンプレートパラメータの制約は必ずしもコンセプトが使用されているわけではありません。レビューの最中にコンセプトを使うように訂正が入ることもあれば、適格要件(Mandate)で文章と式によって指定されることもあります。

この提案は、今後のライブラリ機能はテンプレートパラメータの制約に必ずコンセプトを使用するように提案するとともに、なぜ現在LWG/LEWGがそうしていないのかを明らかにしようとするものでもあります。

この提案は仮に採択されたとしても、標準のプロセスに適用される問題であり、規格書そのものに何か記述が追加されたりするものではありません。

P2796R0 Core Language Working Group "ready" Issues for the February, 2023 meeting

11月に行われたIssaquah会議でWDに適用されたコア言語に対するIssue報告の一覧。

P2797R0 Proposed resolution for CWG2692 Static and explicit object member functions with the same par

明示的オブジェクトパラメータを持つメンバ関数staticメンバ関数の曖昧さを解消する提案。

明示的オブジェクトパラメータとは、C++23で導入されたDeducing thisという機能のことで、thisに相当する引数を明示的に記述してメンバ関数を宣言できる構文のことです。

struct S {
  int n;

  // 明示的オブジェクトパラメータによるメンバ関数宣言
  void f(this S& self, int m) {
    self.n = m;
  }
}

こうして宣言した関数は、普通に使用する分にはメンバ関数のように使用できますが、規格的にはどちらかというと非メンバ関数のような扱いをされています。特に、そのアドレスはメンバ関数ポインタではなく普通の関数ポインタとして取得され、メンバポインタ特有の少し変わった関数呼び出しではなく通常の関数ポインタの用法によって呼び出しができます。

この扱いはstaticメンバ関数と同様であり、現在の仕様のもとでは関数ポインタ経由で呼び出しを行った際のstaticメンバ関数との間の振る舞いに仕様の空白地帯が存在しているようです

struct A {
  static void f(A);
  void f(this A);

  void g();
};

void A::g() {
  // C++23からの問題
  (&A::f)(A()); // #1 ?
  (&A::f)();    // #2 ill-formed

  // 通常の非修飾名関数呼び出し
  f(A());       // ok、(*this).f(A())のような呼び出しになり、static void f(A)を呼び出す
  f();          // ok、(*this).f()のような呼び出しになり、void f(this A)を呼び出す
  // ここでのオーバーロード解決では、次の2つの候補が上がっている
  // static void f(T, A) : 静的メンバ関数(Tは任意のオブジェクトに無変換でマッチする型名)
  // void f(this A)      : 明示的オブジェクトパラメータを持つ関数
  // そして、f(args...)に対して、f(*this, args...)のように探索とオーバーロード解決が行われる
}

この例の#1は適切な候補が見つからないためエラーとなりますが、#2がどうなるのかは規定されていないようです。

この例はクラス定義内(非staticメンバ関数内)からの呼び出し例であり、関数ポインタ経由の呼び出しを普通の関数呼び出しに直した場合は、(thisが見えていることから)明示的オブジェクトパラメータを持たないオーバーロード候補のメンバ関数static/非static)はその引数列の先頭に暗黙のオブジェクトパラメータを受け取るかのように(先頭に引数を1つ追加したシグネチャを持つかのように)扱われます。

これはメンバ関数staticに関わらず統一的に扱ってオーバーロード解決するための仕組み(おそらくは処理や規格の記述の共通化のため)ですが、staticメンバ関数の場合はこの暗黙の第一引数には同じく暗黙的にあてがわれているフェイクのthis引数だけが当てはまり、それを明示的に指定することも参照することもできません。オーバーロード解決においては、staticメンバ関数の暗黙の第一引数はあらゆる型のオブジェクトを無変換で受けられるような型となり、そのマッチングはオーバーロード順位に影響を及ぼしません。

ただしこの扱いは、関数ポインタから呼び出した時には行われず、(&A::f)(A())A::fオーバーロードされていない場合にのみ適切に呼び出すことができるはずです。ただし、現在の規定ではそれすらも曖昧となっているようです。

C++20までは、上記のA::f()が(static問わず)オーバーロードされている場合でも、そもそも関数ポインタを取る時は取得対象の関数が既に確定している必要があり、オーバーロードされている場合はアドレス取得の段階でエラーになるため問題となることはありませんでした。

// C++20までのコードとする

struct A {
  static void f(A) {}

  static void g(A) {}
  void g() {}

  static void h(A) {}
  static void h() {}

  void i() {}

  void call();
};

void A::call() {
  f(A{});       // ok
  (&A::f)(A{}); // ok

  g(A{});       // ok static
  g();          // ok 非static
  (&A::g)(A{}); // ok ただし実装によってはng

  h(A{});       // ok
  h();          // ok
  (&A::h)(A{}); // ng、オーバーロードされておりアドレス取得対象が確定しない

  i();                // ok
  (&A::i)();          // ng、メンバポインタ呼び出しが必要
  (this->*(&A::i))(); // ok、メンバポインタ呼び出し
}

しかし、Deducing thisの導入によって異なる構文を持ちながら実質的に同じようなシグネチャとなりうるメンバ関数宣言構文が追加されたことでこの辺りのことが曖昧になり、その関数ポインタを取得して呼び出そうとしたときにそれらの宣言が衝突するのかどうか(オーバーロードとみなされるのかどうか)が不透明になってしまっているようです。最初の例では、2つのfオーバーロードとみなされるのかが不透明であるため、&A::fからの呼び出しがどうなるのかが不透明となっています((&A::f)()は、仮に呼び出し可能だったとしても適切な候補がないのでどちらにしてもエラー)。

これらの問題はコア言語のIssueとして報告され、この提案はその解決のためのものです。

提案では、次のようなコードを例に、可能な解決として2つのオプションを提示しています。

struct A {
  static void f(A); // #A staticメンバ関数
  void f(this A);   // #B 明治的オブジェクトパラメータを持つメンバ関数

  static void e(A const&); // #C staticメンバ関数
  void e() const&;         // #D 通常メンバ関数


  // クラス外内からの呼び出し
  // 非修飾名呼び出しはメンバ関数も探索する
  void g() {
    // static + 明治的オブジェクトパラメータを持つメンバ関数
    (&A::f)(A()); // #1
    f(A());       // #2 非修飾名呼び出し、暗黙のオブジェクト引数補完が行われる
    (&A::f)();    // #3

    // static + 通常メンバ関数
    (&A::e)(A()); // #4
    e(A());       // #5 非修飾名呼び出し、暗黙のオブジェクト引数補完が行われる
    (&A::e)();    // #6
  }
};

// クラス外部からの呼び出し
// 非修飾名呼び出しは非メンバ関数のみを探索する
void h() {
  // static + 明治的オブジェクトパラメータを持つメンバ関数
  (&A::f)(A()); // #7
  f(A());       // ill-formed、非メンバf()は宣言されていない
  (&A::f)();    // #8

  // static + 通常メンバ関数
  (&A::e)(A()); // #9
  e(A());       // ill-formed、非メンバe()は宣言されていない
  (&A::e)();    // #10
}
  1. #1 #2及び#4 #5はそれぞれ同じ振る舞いをし(staticメンバ関数を呼び出し)、#3 #6はill-formed
    • 非修飾名による関数呼び出し(&A::f)の実際の引数は((A&)*this, A{})であるため、#Aだけが#1#2の有効な候補となる
    • 関数ポインタからの呼び出し時も、thisが見えてれば非修飾名呼び出しと同じ扱いをする
    • staticメンバ関数と明治的オブジェクトパラメータを持つメンバ関数の間でオーバーロード成立を回避する
  2. #1は曖昧、#2staticメンバ関数を呼び出し、#4は曖昧、#5staticメンバ関数を呼び出す
    • #3(候補がない) #6(メンバポインタ呼び出しが必要)はill-formed
    • (&A::e)という式でオーバーロード解決するのではなく、まずオーバーロード候補全てのアドレスを取得し、そのポインタで呼び出し式を解決する
    • #1 #4は曖昧になり、#2#3は異なる結果となる
    • staticメンバ関数と明治的オブジェクトパラメータを持つメンバ関数の間でオーバーロードが成立し、衝突しうる

どちらのオプションでも、現在は行なっていない関数ポインタの取得対象の自動解決を行うようになります。

EWGにおける議論の結果としてはオプション2が選択されたようです。これによって(&function-id-expression)(expr-list)というような関数ポインタによる呼び出し式では、そのポインタ取得先の最適候補を選択する前に、function-id-expressionでの探索結果によるオーバーロード候補集合を、その要素のポインタに減衰させてからオーバーロード解決を行い、その結果でもって&function-id-expressionがどの関数のポインタを取得するのかを決定します。

オプション2では現在に引き続いて、thisが見えているスコープでも関数ポインタからの呼び出しで暗黙のオブジェクトパラメータの自動補完のようなことをしないため、その呼び出し結果はthisが見えているかに関わらず(クラススコープ内外に関わらず)同じになります。よって、#7 #9は曖昧となり、#8 #10はill-formedとなります。

提案より、サンプルコード

struct C {
  void c(this const C&);   // #1
  void c()&;               // #2 暗黙のオブジェクトパラメータを持つ
  static void c(int = 0);  // #3

  void d() {
    c();               // error: #2と#3の間で曖昧
    (C::c)();          // error: 同様
    (&(C::c))();       // error: オーバーロード候補(this->C::c)のアドレスを解決できない(メンバポインタの構文ではないため)
    (&C::c)(C{});      // #1を選択
    (&C::c)(*this);    // error: #2が選択されるがill-formed(メンバポインタ呼び出しが必要)
    (&C::c)();         // #3を選択
  }
};

c()の呼び出し候補は#1~#3全てですが、thisが非const左辺値であることから#2と#3が最適候補となり(staticメンバ関数に補われた暗黙のオブジェクトパラメータはあらゆる型の値を無変換で受け入れオーバーロード順位に影響しない)、両者の順位がつかないため曖昧となります。(C::c)()c()等価な呼び出しになります。

&(C::c)はメンバポインタ取得の構文として不正なのでエラーになります。

(&C::c)(C{})は#1~#3全ての関数ポインタを取得してからオーバーロード解決を行い(それをpとするとp(C{}))、#2も考慮対象となりますが、右辺値を受けられるのは#1のみとなります。

(&C::c)(*this)も上記とほぼ同様の手順を辿り、非const左辺値にベストマッチするのは#2ですが、この場合に非staticの明示的オブジェクトパラメータを持たない関数が選択されるとill-formedと規定されているためエラーになります。

(&C::c)()は取得される関数ポインタをpとするとp()という呼び出しになり、マッチするのは#3のみです。

この提案は、Issue解決であることもあり、すでに2月のIssaquah会議でC++23へ採択されています。

P2798R0 Fix layout mappings all static extent default constructor

std::mdspanのレイアウトマッピングクラスのデフォルトコンストラクタの事前条件を修正する提案。

現在用意されているstd::mdspanのレイアウトマッピングクラスは3種類(std::layout_left, std::layout_right, std::layout_stride)あり、それらの::mapping型はdefault実装のデフォルトコンストラクタを持っています。そして、このデフォルトコンストラクタは何ら事前条件や制約を持っていません。

例えば次のような極端な例を考えてみると

constexpr size_t N = 4'000'000;
std::layout_left::mapping<std::extents<int, N, N>> map;

レイアウトマッピングクラスは多次元インデックスを1次元配列上のインデックスに変換するようなことを行いますが、その際のインデックスの型はExtents::index_typeが使用され、これはstd::mdspanExtentsテンプレートパラメータから与えられます。この型には通常std::extents<I, N, ...>が使用され、レイアウトマッピングクラスが使用するインデックスの型はここのIから取得されます。

上記の例ではインデックスの型はintであり、各次元の静的な要素数Nint型に収まっているものの、それを1次元のインデックスにマッピングすると最大でN * N + Nのような計算を行うことになり、これはオーバーフローします。

非デフォルトのコンストラクタでは事前条件によってこの問題に対処しており、この提案はデフォルトコンストラクタも同様に事前条件を追加することによってこの問題に対処するようにしようとするものです。

ただし、各次元のextent(次元ごとの要素数)に1つでも動的なもの(std::dynamic_extent)を含む場合、それをデフォルト構築するとその次元のextentは0になるため全体の要素数も0、つまり空になるため問題とならず、デフォルトコンストラクタでこの問題があるのはすべてのextentが静的に定まっている場合のみです。

そこで、レイアウトマッピングクラスに指定されたExtentsが全て静的に定まっている(Extents::rank_dynamic() == 0の)場合、その多次元インデックス値はExtents::index_typeで表現可能であること、が適格要件(Mandates)として指定されるようにします。これは3つのレイアウトマッピングクラスすべてに対して指定されます。

これによって、上記のような例はコンパイルエラーになるようになります。

P2799R0 Closed ranges may be a problem; breaking counted_iterator is not the solution

P2406で報告されている問題について、counted_iteratorの変更による解決は間違っていると指摘する提案。

P2406については以前の記事を参照

P2406で報告されているのは、counted_iteratorを特定の範囲に対して使用するとその終端で意図しない振る舞いをする可能性があり、それを修正するためにlazy_counted_iteratorを提案(あるいはそれをcounted_iteratorに適用)しようとしています。

この提案は、それらの問題の本質はC++イテレータモデルにそぐわない閉区間の範囲を半開区間の範囲を扱うために設計されたcounted_iteratorで使用しようとしていることにあり、counted_iteratorは半開区間の範囲に対して適切に設計されているため、閉区間の範囲のためにcounted_iteratorを壊す(あるいはlazy_counted_iteratorを追加する)のは間違っている、とするものです。

C++イテレータは任意の要素からなる半開区間[first, last))を表現するものであり、N個の要素からなる範囲にはN+1個のイテレータの値が対応しています。この内N個はN個の要素に対応し、残りの1つは終端(番兵)値に対応します。C++20の範囲(range)は、このようなイテレータによる範囲の先頭イテレータと終端イテレータのペアとなるもののことであり、これもやはり半開区間の範囲を表現しています。

counted_iteratorは整数カウントをイテレータに結びつけただけのものです。

template <input_or_output_iterator It>
struct counted_iterator {
  It it;
  int count;
};

counted_iteratorの移動と共にカウントは増減するため、同じ範囲への2つのイテレータ間の距離を簡単に計算できます。

counted_iteratorがカウントダウン(進行するとカウンタを減らす、後退する場合は逆)によってカウントを管理しているのは単にその番兵をステートレスにするためで、終端チェック(番兵値との比較)においてはカウンタが0かどうかをチェックするだけで済むためです。

ここでP2406で提起されている問題に戻ると、これらの問題を引き起こしている入力の範囲は半開区間ではなく閉区間を想定するものであることに気付けます

iota | filter | takeの例

for (auto i  : std::views::iota(0)
             | std::views::filter([](auto i) { return i < 10; })
             | std::views::take(10))
{
  std::cout << i << '\n';
}

ここでiota(0) | filter([](auto i) { return i < 10; })によって生成される範囲はちょうど10個の要素を持ちますが、番兵値に対応する11個目の要素がありません。したがって、その終端を得るために11個目の要素(イテレータ)を計算しようとしていることが問題の根本的な原因です。

istream_viewの例

auto iss = std::istringstream("0 1 2");
for (auto i : std::ranges::istream_view<int>(iss)
            | std::views::take(1))
{
  std::cout << i << '\n';
}

auto i = 0;
iss >> i;
assert(i == 1); // FAILS, i == 2

istream_viewtake(1)によってストリームから1つ値を読み取り(0)それを要素とする範囲を生成しますが、ここでもやはり終端値としてその次の値(1)を必要とするため、このイテレーションの終了までの間にストリームから2つの値を読み出すことになります。

ここでの問題は、istream_viewがその入力ストリームへの全てのアクセスがそのイテレータを介してしか行われないことを前提としていることから起きており、イテレータを一貫して使用している限り問題は起きません。つまり、元のストリームを直接触りに行く利用者が、istream_view | take(1)による範囲が半開区間(1要素+1番兵)ではなく閉区間(1要素)だと思ってしまっていることから起きています。前述のようにこの期待は間違っています。

ただ、閉区間による範囲を考慮すると便利な場合もあり、例えばiota_viewはその要素型の最大値を範囲内に含めることができません(番兵値として必要となるため)。ですが、閉区間による範囲は現在のC++イテレータモデルにはそぐわないものであり、counted_iteratorがそのために設計されていないことは当然のことです。

とはいえ、P2406で提起されている問題にもあるように、閉区間の範囲も出現しうるものであり、それをC++イテレータに適合させて使用しようとするのも自然なことではあります。その場合に必要となるのは、そのようなちょうどN個の要素からなる閉区間をN+1個の要素による半開区間として扱うための方法です。つまり、番兵となる何かの値を添加する必要があります。

その方法としては、例えばvariant<OriginalIterator, PastTheEndSentinel>のような型の値を使用するなどの方法が考えられ、カウントを使用するのもその方法の一つです。ただし、カウントを使用するのはそのための唯一の方法ではありません。例えば、整数の閉区間の範囲[first, last]を表すrange-v3ライブラリのclosed_iotaイテレータは次のようなものになっており

struct iterator {
  I current;
  I last;
  bool past_the_end;
};

このイテレータは進行によってcurrent == lastとなる場合にpast_the_endtrueに設定し、currentをインクリメントしないようにしています。

区間の範囲を表現し、それを既存のイテレータ/rangeとして使用できるようにすることには価値がある可能性があります。しかし、counted_iteratorがそのための方法になるべきではありません。特に、そのためにオーバーヘッドを増やし、コンパイラの最適化を阻害し、機能を制限するような変更をC++23作業完了の直前に行うことは閉区間の範囲のサポート方法としては完全に誤っています。

counted_iteratorは半開区間の範囲に対してカウントを結びつけるように設計されており、その想定されるユースケースと設計に問題はなく、閉区間の範囲のサポートのためにはC++26で別の機能として追加することができます。

P2802R0 Presentation of P1385R7 to LEWG at Issaquah 2023

P1385R7をLEWGのメンバにプレゼンするためのスライド。

P1385R7は行列型をはじめとする線形代数関連のクラス型を標準ライブラリに用意しようとする提案です。詳しくは以前の記事を参照

R0からR7の変遷や、今後の展望などが簡単にまとめられています。

P2803R0 std::simd Intro slides

C++26に向けて提案中のstd::simdクラス型の紹介スライド。

std::simdについては以前の記事を参照

データ並列型としてのstd::simdのコンセプトから、基本的な使用方法まで非常にわかりやすく紹介されています。

P2805R0 fiber_context: fibers without scheduler - LEWG slides

P0876で提案中のスタックフルコルーチンの中核となるfiber_contextの紹介スライド。

P876については以前の記事を参照

fiber_contextの役割や必要性を紹介し、用意されているコンストラクタやメンバ関数について解説が行われています。

P2806R0 do expressions

値を返せるスコープを導入するdo式の提案。

C++の構文は文(statement)に基づいて構築されており、ifforなどは文であり式(expression)ではありません。文は基本的に値を返すことができず、単一の式は条件分岐やループなどを含むことができません。

変数の初期化時など、単一の式以上のことが必要になる場合はそれを関数にまとめて関数呼び出しに置き換えることで近いことを達成でき、特に即時呼び出しするラムダ式を使用するとかなり通常のブロックに近い形で書くことができます。

int main() {
  // 実行時に決定される値とする
  bool flag = ...;

  // 変数をconstで宣言したいが、初期化はflagによって分岐する
  const int n = [&] {
    if (flag) {
      ...
      return -10;
    } else {
      ...
      return 10;
    }
  }();

}

ただし、この方法も問題があり、追加の関数スコープを導入してしまうことで制御フローを複雑化させています。このようなラムダ式forループの内部に現れている場合、ラムダ内部から外側のループをbreak/continueすることはできず、関数内部で現れている場合はラムダを囲む関数から直接returnすることはできないほか、コルーチン内部で現れている場合はラムダ内部から外側コルーチンのco_yield/co_await/co_returnを行うことができません。このような制約を理解するには追加のC++に関する知識が求められるなど、この方法は完璧とは言い難いものです。

この問題はまた、提案中のパターンマッチング(P1371R3)においても問題となる可能性があります。パターンマッチングにおいてはinspect式のブロック中にpattern => expression;の形でパターンに対する処理を記述していきますが、ここでも=>の右辺に指定できるのは単一の式であり、先ほど同様に1つの式以上のことが必要になった時に関数呼び出しに置き換えるなどする必要があります。

P1371ではそのために、=>の右辺に現れる{ statement }を特別扱いして、この式をvoid型の式として評価することで1つの式以上のことを書けるようにしています。ただしこれは言語の他の部分と一貫性がなく、パターンマッチングが{}初期化式を使用する将来の拡張を妨げることになります。

auto f() -> std::pair<int, int> {
  // これはできる
  return {1, 2};

  // P1371(P2688)の方向性だとこれはできない
  return true match -> std::pair<int, int> {
      _ => {1, 2}
  };
}

このパターンマッチングにおける問題を解決するには、ステートメント式(statement-expression)が必要です。そして、そのようなものはパターンマッチングだけではなく、最初の例のように広く有用なものになります。

このように、現在及び将来の機能でもステートメント式が必要とされていることから、パターンマッチングにおける文法を単純化できるような構文でステートメント式をサポートする直交性の高い言語機能を導入しようとするのがこの提案の目的です。

この提案によるステートメント式は、do { statement }のような構文で、do式と呼ばれます。do式は式なので型と値を持ち、とても単純には次のように使用できます

int x = do { do return 42; };

do式のブロックが導入するのは単なるブロックスコープであり、そこから値を返すにはdo returnというreturn文を使用します。ブロックスコープ中には他のブロック同様に任意のC++コードを記述することができ、これは関数スコープではないため新しいスタックフレームを導入せず、その外側の制御フローの一部であり続けます。

do returnは、do式から値を返すという点を除いてreturnと同じ振る舞いをし、コピー省略などの戻り値最適化も適用されます。

std::string s = do {
  std::string r = "hello";
  r += "world";
  do return r;  // rは暗黙ムーブされる
};

do式の型と値カテゴリは、その内部のすべてのdo return文から推定されます。これはautoで戻り値型を宣言した関数/ラムダ式と同じルールによります。また、それらと同様に後置戻り値型を書くことで戻り値型を明示的に指定することもできます。

do -> long { do return 42; }  // do式の戻り値型はlong、値カテゴリはprvalue

do式内でdo returnが使用されていないか、do return;のようにしか使用されていない場合、式の型はvoidpravalueになります。

P2688R0で提案されているパターンマッチング構文(P1371の進化版)にこのdo式を組み込むことができ、それによって次のような記述が変化します

P2688 この提案
x match {
  0 => { cout << "got zero"; };
  1 => { cout << "got one"; };
  _ => { cout << "don't care"; };
}
x match {
  0 => do { cout << "got zero"; };
  1 => do { cout << "got one"; };
  _ => do { cout << "don't care"; };
}

どちらの場合も、このmatch式の結果はvoidです。

これによって、=>の右辺での{}の特別扱いを回避し、{}初期化をサポートすることができるようになります

P2688 この提案
auto f(int i) {
  return i match -> std::pair<int, int> {
    0 => {1, 2};          // ill-formed
    _ => std::pair{3, 4}; // ok
  }
}
auto f(int i) {
  return i match -> std::pair<int, int> {
    0 => {1, 2};          // ok
    _ => std::pair{3, 4}; // ok
  }
}

do式の導入するスコープは関数スコープではなく、制御フローはその外側のフローの一部です。従って、do式からのreturnbreakはその外側の制御フローに対して効果を持ちます。

int outer() {
  int g = do {
    if (cond) {
      do return 1;  // do式からのreturn
    }

    return 3; // outer()からのreturn
  };
}

void func() {
  for (;;) {
    int j = do {
      if (cond) {
        break;  // do式およびその外側のループから脱出
      }

      for (something) {
        if (cond) {
          do return 1;  // このループから脱出し、do式からreturn
        }
      }

      do return 2;  // do式からのreturn
    };
  }
}

このdo式の導入するスコープは関数スコープ(関数・ラムダ・コルーチン)と異なる点があり、do式がvoid型の式ではなくdo returnが現れる前にその終端にたどり着いた場合はill-formed とされます。関数スコープの場合はこれは未定義動作とされていました。これによって、ユーザーはdo式内の制御パスを全てカバーするように注意する必要があり、それができていないとコンパイラに怒られます。

int i = do {
  if (cond) {
    do return 0;
  }

  // error
};

do式の型がvoidの場合は暗黙的にdo return;が補われエラーにはなりません。

また、do return以外にも、do式終端にたどり着かないことがわかっているものが全ての制御パスに現れていればエラーにはなりません。それは例えば

  • 外側の制御フローへ戻るもの
    • return
    • breake
    • continue
    • co_return
  • throw
  • [[noreturn]]関数
    • 現在でも[[noreturn]]とマークされた関数から制御が戻ると未定義動作となる
    • この未定義動作を利用して、新しい未定義動作を導入することなく[[noreturn]]関数の呼び出しを制御フローからの脱出と見做せる
enum Color {
  Red,
  Green,
  Blue
};

void func(Color c) {
  // error
  std::string_view name = do {
    switch (c) {
      case Red:   do return "Red"sv;
      case Green: do return "Green"sv;
      case Blue:  do return "Blue"sv;
    }
    // ここに到達しうる
  };
}


int main() {
  // ok
  auto a = do {
    if (cond) {
      do return 1;
    } else {
      do return 2;
    }
    // ここにはこない
  };

  // ok
  int f = do {
    if (cond) {
      do return 1;
    }

    throw 2;
    // ここにはこない
  };
  
  // ok
  int h = do {
    if (cond) {
        do return 1;
    }

    std::abort(); // [[noreturn]]関数
    // ここにはこない
  };
}

また、goto文の使用はdo式からの脱出のみ許可され、do式内のラベルへのジャンプは禁止されます。

この提案はEWGでの最初のレビューにおいて引き続き議論していくことにコンセンサスが取れています。

P2807R0 Issaquah Slides for Intel response to std::simd

std::simdに対するintelの経験に基づくフィードバック提案の解説スライド。

対象の提案については以前の記事を参照

かく提案のうち既に解決済みの問題や、未解決のものの理由や利点欠点などを解説しています。

P2808R0 Internal linkage in the global module

グローバルモジュールにある内部リンケージを持つエンティティの曝露(exposure)を許容するようにする提案。

static inline関数のような、内部リンケージを持つエンティティを名前付きモジュールの本文内で使用する場合、それをその翻訳単位の外部に曝露しないように注意しなければなりません。内部リンケージを持つものの曝露とは、exportしたinline関数などから内部リンケージを持つものが翻訳単位外部から参照される可能性がある場合を言います。曝露が起きている場合はコンパイルエラーとなります。

曝露については以前の記事を参照

ただ、static inline関数のような内部リンケージエンティティは特に、次のような理由からCヘッダで一般的に使用されています

  • CのinlineセマンティクスはC++のそれとは異なり、どこかに非inline定義が必要となる
  • 内部リンケージを持つことから、コンパイラが認識したものと異なるものを取得することがなく、ABIの分離を実現できる
    • これによって、ABIの破損を気にすることなくその動作を変更できる

このようなCヘッダはC/C++間のコード共有のためのものでもあり、そのままモジュールに移行されることはなく、おそらくヘッダユニットのインポートやグローバルモジュールフラグメントでのインクルードなどによって名前付きモジュール内から利用されることになるでしょう。

その場合、ヘッダで定義されている内部リンケージエンティティ(特にstatic inline関数)はグローバルモジュールに属する内部リンケージエンティティとなりますが、名前付きモジュールのインターフェースからの暴露に関する制約は名前付きモジュールに属する内部リンケージエンティティと同じ扱いとなります。従って、そのようなものを普通に使う感覚でinline関数や関数テンプレートから参照してしまうとコンパイルエラーを引き起こします。

/// myheader.h

// 内部リンケージ
static inline int f(int n) {
  ...
}
/// mymodule1.cpp
module;
// グローバルモジュールでインポート
#include "myheader.h"
export module mymodule1;

export inline int func1(int n) {
  return f(n);  // ng、内部リンケージエンティティf()を曝露している
}

inline int func2(int n) {
  return f(n);  // ng、内部リンケージエンティティf()を曝露している
}

static inline int func3(int n) {
  return f(n);  // ok、func3()は内部リンケージ
}
/// mymodule2.cpp
export module mymodule2;

import std;

// ヘッダユニットでインポート
import "myheader.h"

template<std::integral I>
export inline int func1(I n) {
  return f(n);  // ng、内部リンケージエンティティf()を曝露している
}

// 以下同様

重要なのは、このようなものはC/C++のコード共有地点で現れるもので、ABI分離等の利点があり、C++の都合のみで変更できるものではなく、また名前付きモジュールに属していない(名前付きモジュールの一部ではない)ということです。さらに、そのようなC/C++共有ヘッダの内部リンケージエンティティはGithubで公開されているいくつかの大規模なプロジェクトだけでも数千件も発見でき、潜在的にはさらに多く利用されていることが予想されます。

P2691R0ではヘッダユニットのstatic inline関数に限って同様の問題を報告しており、そこではこの問題は以前に予想されたよりも影響が大きく、深刻なモジュール採用の障害になっていることを報告しています。

この問題を要約すると、Cヘッダにある内部リンケージエンティティをグローバルモジュールを介して名前付きモジュールのインターフェースから使用する際、曝露を回避して使用しなければならない(あるいはそれが困難)、ということです。この提案は、そのようなものを含む既存コードーベースの円滑なモジュールへの移行のために、この問題を解決しようとするものです。

この問題の解決のためには、次のことを達成する必要があります

  • 問題となっているエンティティを他の翻訳単位から参照できるようにする
  • 既存コードを壊さない
  • UB(すなわちODR違反)を増加させない
  • 実装可能であること

その上でこの提案では、次のような変更によってグローバルモジュールにある内部リンケージエンティティが名前付きモジュールインターフェースから曝露されるのを許可します

  1. インポート可能なヘッダをインポートするすべての翻訳単位は、そのヘッダの独自のヘッダユニットを取得する
    • これはモジュールであるかに関わらない
  2. 各翻訳単位のグローバルモジュールフラグメントとすべてのヘッダユニットに対して、次の変換を適用する
    • 内部リンケージを持つすべてのエンティティは、インポート先の翻訳単位に属するモジュールリンケージが与えられる
    • 内部リンケージを持つすべてのエンティティは、この変換が適用されない場合のエンティティと区別される
      • 何かしらのタグを用いて名前マングルされる
    • 内部リンケージを持つすべての関数と変数はinline化される

1つ目の変更によって、ヘッダユニットのインポートは翻訳単位ごとに異なるヘッダユニットを生成し使用するようになります。これによって、翻訳単位が異なれば同じヘッダを示すヘッダユニットをインポートしていても、異なるヘッダユニット(翻訳単位)を使用することになります。

2つ目の変更は、主にモジュールのインターフェース単位において行われ、Cヘッダからの内部リンケージエンティティはグローバルモジュールではなくそのモジュールに属するモジュールリンケージを持つエンティティとして扱われ(これによって同じモジュール内の別の翻訳単位から使用できるようになり)、かつマングル名(モジュール内部でのみ有効な)レベルで明確に区別されるとともに、関数と変数はインライン展開されることで直接定義を参照することを回避します。ただし、この変換はどうやら非モジュールにおいても行われるようで、その場合はその翻訳単位とインポートするヘッダユニットを含む匿名モジュールが生成されたかのような扱いをされるようです。

この解決策は、先ほどの4つの要件を全て満たしています。例えば、内部リンケージのエンティティはモジュールのインターフェースでモジュールリンケージを持つようになり曝露の制限対象から外れますが、翻訳単位ごとの実体生成とインライン化によりABI分離も保たれています。実装に関しては、少し問題があるものの主要なコンパイラの開発者から実装可能であるとの確認を取れているようです。

P2810R0 is_debugger_present is_replaceable

P2546で提案されているis_debugger_present()をユーザーが置換可能にする提案。

std::is_debugger_present()は実行時にデバッガがアタッチされている場合にtrueを返す関数で、これはフリースタンディング環境でもサポートされることを目指しています。しかし、組み込み環境などの一部のフリースタンディング環境ではこの実装が困難となる場合があります。

この機能はフリースタンディング環境でも有用である可能性があり、フリースタンディング環境で削除してしまうとその判定と代替手段のためにプリプロセッサが使用されることになり、C++エコシステムのCPP依存を高めます。

そのため、そのような環境でもこの関数を動作させるための方法が必要であり、この提案はその方法としてこの関数をユーザーが置き換えることを許可することを提案しています。

これによって、ユーザーはフリースタンディング環境以外の環境においても、そのユースケースに従ってstd::is_debugger_present()を柔軟にカスタマイズすることができるようになります。例えば

  • アプリケーションの検証ビルドではtrueを返すようにしておく
  • 外部入力によって結果を制御する
    • キー入力やその他の外部信号、シグナルハンドラのシグナルなど

ここでの置き換えとは、std::is_debugger_present()という関数シグネチャを衝突させる形でユーザーコードで定義し、実装はそれを検出したらデフォルトの実装をユーザー定義のものに置き換える、のようなことです。

P2812R0 P1673R11 LEWG presentation

P1673で提案中の線形代数ライブラリの解説を行う文書。

主に、LEWGのレビューにおいてその設計がどのように変化したかを記述しています。

P2815R0 Slides for presentation on P2188R1

P2188R1の解説スライド。

P2188R1で主張されているポインタの保証や意味論(必ずしも現在のC++が保証していないもの)についての詳しい解説がなされています。

P2188R1については以前の記事を参照

P2816R0 Safety Profiles: Type-and-resource Safe programming in ISO Standard C++

C++を安全なプログラミング言語へと進化させることについて、その必要性及び方法について解説したスライド。

主に、P2687で提案されていることのベースとなっている考えについて詳細に説明されています。

おわり

この記事のMarkdownソース

[C++] constexpr ifとコンセプトと短絡評価と

constexpr if(構文としてはif constexpr)の条件にはboolに変換可能な任意の定数式を使用できます。複数の条件によって分岐させたい場合、自然に&&もしくは||によって複数の条件式をつなげることになるでしょう。そしてその場合、条件式には左から右への評価順序と短絡評価を期待するはずです。

auto func(const auto& v) {
  return v; // コピーされる
}

template<typename T>
void f(T&&) {
  // Tが整数型かつfuncで呼び出し可能
  // Tが整数型ではない場合は右辺の条件(funcの呼び出し可能性)はチェックされないことが期待される
  if constexpr (std::integral<T> && std::invocable<decltype(func<T>), T>) {
    ...
  } else {
    // Tがunique_ptrの場合はこちらに来ることが期待される
    ...
  }
}

int main() {
  std::unique_ptr<int> up{};

  f(std::move(up)); // ng
}

エラーはfunc()returnvがコピーできないために起きており、ここでのvstd::unique_ptrconst左辺値なので当然コピーはできません。しかし、コードのどこでもfunc()std::unique_ptrで呼んではいません、なぜこれはエラーになるのでしょうか?

起きていること

コードをよく見ると、直接的ではないもののfunc()std::unique_ptrで呼んでいる箇所が1つだけあります。それは、std::invocable<decltype(func<T>), T>という式で、これはstd::invocableコンセプトによってTによってfunc<T>()が呼び出し可能かを調べています。関数f()の引数型T経由で、ここでstd::unique_ptrfunc()にわたっています。

(コンセプトはこの様に使用した時にそれ単体で1つの式になり、bool型のprvalueを生成する定数式となります)

とはいえ、その直前(左辺)の条件ではstd::integral<T>によってTが整数型であることを要求しており、std::unique_ptrは当然整数型ではないのでその条件はfalseとなり、短絡評価によってstd::invocable<decltype(func<T>), T>は評価されないことが期待されます。しかし、ここでfunc()std::unique_ptrがわたってエラーが起きているということは、どうやらその期待は裏切られている様です。

このことは、std::integral<T>を恒偽式に置き換えてやるとよりわかりやすくなります。

template<typename T>
void f(T&&) {
  if constexpr (false && std::invocable<decltype(func<T>), T>) {
    ...
  } else {
    // Tがunique_ptrの場合はこちらに来ることが期待される
    ...
  }
}

どうやらconstexpr ifの条件式では短絡評価が起きていない様に見えます。

そしてこのことは主要な3コンパイラで共通していることから、どうやら仕様に則った振る舞いの様です。

if constexpr (A && B)としたら、A -> Bの順番で評価されてほしいしA = falseならBは評価されないでほしい気持ちがあります(これは||でも同様でしょう)。そうならないのが自然な振る舞いなはずはありません・・・

constexpr ifの扱い

constexpr ifC++17で導入された新しめの言語機能であり、ユーザーフレンドリーになってることが期待されます。しかし実際にはconstexpr ifif文の一種でしかなく、構文的にはconstexprがあるかないか程度の違いであり、その条件式の扱いはifのそれに準じています(そのため、実はcostexpr ifにも初期化式が書けます)。では、ifの条件式の扱いがそうなっているのでしょうか?

ifの条件式に関しては、文脈的にbool変換可能な式という程度にしか指定されておらず、constexpr ifに関してはそれが定数式であることが追加で要求されるくらいです。つまり、if/if constexprの条件式で短絡評価が起こるのかにはifは関与していません。

文脈的にbool変換可能な式というのはめちゃくちゃ広いですが、今回問題となるのはその中でも&&||の2つだけです(前述のように演算子オーバーロードは考慮しません)。実は&&||の組み込み演算子が短絡評価するかどうかは実装定義だったりするのでしょうか?

これもそんなはずはなく、(A && BもしくはA || Bとすると)どちらの演算子もそのオペランドの評価順序はA -> Bの順で評価されることが規定され、なおかつ短絡評価に関してもきちんと規定されており未規定や実装定義などではありません。C++適合実装は、場所がifであるかどうかに関係なく、A && BもしくはA || Bという式の実行において、式Aの結果に応じた短絡評価を行わなければなりません。

さて、ここまで掘り返しても冒頭のエラーの原因がわかりません。一体何が起きているのか・・・?

コンセプトはテンプレート(重要!!)

回り道をしましたが、実はここで起きていることはそれら以前の問題です。

template<typename T>
void f(T&&) {
  // Tが整数型かつfuncで呼び出し可能
  // Tが整数型ではない場合は右辺の条件(funcの呼び出し可能性)はチェックされないことが期待される
  if constexpr (std::integral<T> && std::invocable<decltype(func<T>), T>) {
    ...
  } else {
    // Tがunique_ptrの場合はこちらに来ることが期待される
    ...
  }
}

この局所的なコードのコンパイルにおいて、コンパイル時には次の順番で処理が実行されます

  1. f()インスタンス
  2. cosntexpr ifの条件式のインスタンス
  3. constexpr ifの条件式の評価
  4. constexpr ifの分岐

この時、2番目の条件式のインスタンス化においては主に、テンプレートパラメータTに依存するもののインスタンス化が行われます。インスタンス化が必要なものとはつまりテンプレートのことであり、そこにはコンセプトも含まれています。そう、コンセプトはテンプレートの一種なので、単体の定数式として評価する前にコンセプトそのもののインスタンス化が必要となります。

この例でのコンセプトのインスタンス化では、std::integral<T>std::invocable<decltype(func<T>), T>の2つのコンセプトのインスタンス化が行われます。前者は今回関係ないことが分かっているので、後者を詳しく見てみます。

まず、std::invocableの定義は次のようになっています

template<class F, class... Args>
concept invocable = requires(F&& f, Args&&... args) {
  invoke(std::forward<F>(f), std::forward<Args>(args)...);
};

コンセプトの定義は基本的にはこのrequires式内に求める要件式を並べて行い(また、複数のrequires式をつなげることもでき)、コンセプトのインスタンス化に伴って、その字句順(requires式の並び順)(たぶん)にテンプレートパラメータの置換(substitution)とテンプレートのインスタンス化(以降単にインスタンス化)が行われていきます。その際、requires式内部ではインスタンス化に伴ってill-formed(コンパイルエラー/ハードエラー)になるようなことが起きたとしても、そのrequires式がその式の評価時にfalseとなるだけでハードエラーを起こしません。

requires式のインスタンス化とその式の値の評価も、注意深く規格署の記述を読むと、テンプレートパラメータ置換およびインスタンス化と評価は段階が異なることが読み取れ(る気がし)ます。

この時、次のような記述([expr.prim.req]/5)によって、インスタンス化された式がハードエラーを起こす場合がある事が指定されています

requires式内の要件式に無効な型や式を含むものが含まれていて
それがtemplated entityの宣言内に表示されない場合
プログラムはill-formed

templated entityというのはテンプレート定義内の何かの事で、ここではrequires式に含まれる色々なものの事です。その宣言内に含まれない場合というのは要するに、コンセプト定義に直接見えていないような場合ということで、requires式に書かれた各要件式の内部で、別の式等が何かハードエラーを起こす場合のことを言っています(とおもいます・・・)。

さて、std::invocableは1つのrequires式だけからなるコンセプトで、その1つのrequires式は1つの単純要件だけを持ち、それはstd::invokeによってFArgsで呼び出し可能かどうかを調べています。

std::invocable<decltype(func<T>), T>の場合、最終的にはfunc<std::unique_ptr>()が呼び出し可能かが問われることになります。そして、そのチェックは宣言のみのチェックではなく、invoke(std::forward<F>(f), std::forward<Args>(args)...);という式の有効性のチェックとなるため、テンプレートパラメータの置換とそのインスタンス化が発生します。

func<std::unique_ptr>()インスタンス化されると、そのreturn文でコピーができないことからハードエラーを起こしますが、それはまさにinvocableコンセプト定義のrequires式の要件式に直接現れずにその呼び出し先の関数本体内で発生するため、これはrequires式の範囲外となりそのままハードエラーになります。

冒頭の例の謎のエラーはまさに、この場合に起こるエラーです。つまりは、式として短絡評価されるか以前のところ(テンプレートのインスタンス化)でエラーが起きています。

この場合にconstexpr ifに求めるべきだったのは、その条件式においてその評価順序に応じたインスタンス化と短絡評価によるインスタンス化そのもののスキップだったわけです。当然そんなことはC++17でも20でも要求されておらず、現在のコンパイラはそのようなことはしてくれません。想像ですが、インスタンス化そのものがスキップされると定義の存在有無が変化し、それはひいてはODRに関係してくる気がします。

回避手段

とはいえ、constexpr ifコンパイル時の条件によってインスタンス化の抑制を行うものなので、その条件式はほとんどの場合に何かしらのテンプレートパラメータに依存することになるでしょう。テンプレートのインスタンス化に伴うハードエラー回避のために短絡評価を期待するのはある種自然な発想であるため、この問題は割と深刻かもしれません(それでも、実行時ifと異なり問題はコンパイル時の謎のエラーとして報告されるのでマシではあります)。

そのため、短絡評価を期待通りに行ってもらう方法を考えてみます。

関数に制約をかける

この場合の例にアドホックな解決策ですが、呼び出そうとする関数が適切に制約されていることによってstd::invokeの呼び出し先がなくなれば、このような場合にstd::invokeの内部の呼び出し先内でのエラー発生を回避できます。

template<typename T, typename F>
void f(T&&, F&&) {
  // Tが整数型かつfuncで呼び出し可能
  // Tが整数型ではない場合は右辺の条件(funcの呼び出し可能性)はチェックされないことが期待される
  if constexpr (std::integral<T> && std::invocable<F, T>) {
    
  } else {
    // Tがunique_ptrの場合はこちらに来ることが期待される
  }
}

int main() {
  std::unique_ptr<int> up{};

  f(std::move(up), [](const std::copyable auto& v) { return v; }); // ok
  f(std::move(up), [](const               auto& v) { return v; }); // ng
}

例示のために少し書き換えています。

この場合、std::invocableコンセプト定義内のrequires式内のstd::invokeによる制約式では、呼び出すべき関数が見つからない(std::unique_ptrstd::copyableではない)ことからstd::invokeそのものがエラーになり、それはrequires式の範囲内なのでハードエラーになる代わりにそのrequires式の式としての評価結果がfalseになり、std::invocable<F, T>falseに評価されます。

とはいえ、このような回避手段はこの問題を理解したうえで適用可能であるかを調べる必要があり、いつでも可能な汎用的なソリューションではありません。

条件式を分ける

&&でつながれた条件式であれば複数のif constexpr文に分割することができるかもしれません。

auto func(const auto& v) {
  return v; // コピーされる
}

template<typename T>
void f(T&&) {

  if constexpr (std::integral<T>) {
    // Tがunique_ptrの場合はここには来ない
    if constexpr (std::invocable<decltype(func<T>), T>) {
      ...
    }
    ...
  } else {
    // Tがunique_ptrの場合はこちらに来ることが期待される
    ...
  }
}

int main() {
  std::unique_ptr<int> up{};

  f(std::move(up)); // ok
}

costexpr iffalseとなる(選ばれなかった)ステートメントインスタンス化されません。そのため、costexpr ifがネストしていれば疑似的に短絡評価のようになります。

この場合は、分岐が増えることによって考慮すべきパスが増加することに注意が必要です

template<typename T>
void f(T&&) {

  if constexpr (std::integral<T>) {
    // Tがunique_ptrの場合はここには来ない
    if constexpr (std::invocable<decltype(func<T>), T>) {
      ...
      return;
    }
    ...
  } else {
    // Tがunique_ptrの場合はこちらに来ることが期待される
    ...
    return;
  }

  // ここに来る場合がある
  std::unreachable();
}

あと||はどうしようもありません。ド・モルガンで頑張れる可能性はありますが・・・

requires式と入れ後要件を使う

@yohhoyさんご提供の方法です。

auto func(const auto& v) {
  return v; // コピーされる
}

template<typename T>
void f(T&&) {

  if constexpr (requires { requires std::is_integral_v<T>; requires std::invocable<decltype(func<T>), T>; }) {
    // Tがunique_ptrの場合はここには来ない
    ...
  } else {
    // Tがunique_ptrの場合はこちらに来る
    ...
  }
}

int main() {
  std::unique_ptr<int> up{};

  f(std::move(up)); // ok
}

実はrequires式はコンセプト定義やrequires節の外側でも書くことができて、その場合も内部の要件をチェックした結果のbool型のprvalueを生成する定数式になります。そのため、costexpr ifの条件式でも使用することができます。

上記のrequires式を取り出して見やすくすると

requires { 
  requires std::is_integral_v<T>;
  requires std::invocable<decltype(func<T>), T>;
}

requires式内部でrequires expr;のようにしているこの書き方は入れ子要件の制約と呼び、そのインスタンス化及び評価の順序は、字句順すなわちこのようにフォーマットした場合の上から順番に行われます。そのため、実質的にこれは&&条件を書いたのと同様になっており、なおかつコンセプトのrequires式はその要件を満たさない(falseに評価された)式が出現するとそこでインスタンス化と評価を停止するため短絡評価が期待できます。

ただ、短絡評価に関しては、「requires式の結果を決定する条件に出会うと、インスタンス化と評価を停止する」のように規定されており短絡評価を指定しているかは微妙です。実際、MSVCは短絡評価をしないような振る舞いをする場合があります(上記の例ではMSVCでも回避可能でしたが)。

また、これはも||で使用できません。ド・モルガンで頑張ることはできるかもしれませんが。

コンセプトに埋め込む

@yohhoyさんご提供の方法その2です。

前項の方法のrequires式を1つのコンセプトに纏めてしまう方法です。

#include <iostream>
#include <concepts>
#include <memory>

auto func(const auto& v) {
  return v; // コピーされる
}

// constecpr if条件を抽出したコンセプト定義
template<typename T>
concept C = std::integral<T> && std::invocable<decltype(func<T>), T>;

template<typename T>
void f(T&&) {

  if constexpr (C<T>) {
    // Tがunique_ptrの場合はここには来ない
  } else {
    // Tがunique_ptrの場合はこちらに来ることが期待される
  }
}

int main() {
  std::unique_ptr<int> up{};

  f(std::move(up)); // ok
}

コンセプト定義内でrequires式を用いずに直接bool型の定数式を指定することもできます。この場合のインスタンス化と評価の順序の規定は複雑ですが、コンセプトの定義内でこのように書いている場合は&&でも||でも同様に、字句順あるいは左から右にインスタンス化と評価が進行します(またこれは、requires式が複数ある場合のインスタンス化と評価の順序と同様です)。

さらに、requires式内部ではなくコンセプト定義で直接このように書いた場合は、&&||の結果に従った適切な短絡評価が規定されています。従って、この場合はstd::integral<T>falseと評価されればstd::invocable<decltype(func<T>), T>は評価もインスタンス化もされません。

前項のrequires式と入れ後要件による方法は移植可能性に懸念があるのと、||で使用できないのが問題でしたが、この方法であればそれらの問題点はすべて解消され、インスタンス化と評価時に短絡評価が保証されます。

std::conjunction/std::disjunctionを使用する

前項のコンセプトに切り出してしまう方法はほぼ完璧な方法ですが、コンセプトが名前空間内でしか定義できないことから、名前空間を汚すのが忌避されるかもしれません。短絡評価を行いつつ条件式はその場での使い捨てにしたい場合の方法として、古のTMPのユーティリティであるstd::conjunction/std::disjunctionを使用する方法があります。

#include <iostream>
#include <concepts>
#include <memory>

auto func(const auto& v) {
  return v; // コピーされる
}

// func<T>のインスタンス化を遅延させるラッパ型
template<typename T>
struct C {
  static constexpr value = std::invocable<decltype(func<T>), T>;
};

template<typename T>
void f(T&&) {

  if constexpr (std::conjunction_v< std::is_integral<T>, C<T> >) {
    // Tがunique_ptrの場合はここには来ない
  } else {
    // Tがunique_ptrの場合はこちらに来ることが期待される
  }
}

int main() {
  std::unique_ptr<int> up{};

  f(std::move(up)); // ok
}

std::conjunctionおよびstd::disjunctionは、T::valuebool型定数であるメタ関数を任意個数受けて、その&&/||を求めるメタ関数ですが、その評価に当たっては短絡評価が規定されています。そのため、条件をメタ関数に落とすことができるならば、この方法を用いてもインスタンス化を短絡評価させることができます。

ただ、この例はあまり適切ではない例で、func<T>の出現(インスタンス化)を遅延させるために別の型に埋め込まなければならなくなっており、どのみち名前空間に不要なものがばらまかれてしまっています・・・

最後にこの古のメタ関数にたどり着いたところで、冒頭の例の問題とはまさにこれらのメタ関数が導入された理由の一つでもある事に気づきます。つまり、T1::value && T2::value && T3::value && ...というようにすると、この式の評価よりも前にTn::valueインスタンス化が要求されてしまい、そのいずれかがハードエラーを起こす場合にまさに冒頭の例と同じ問題にぶつかります。std::conjunctionおよびstd::disjunctionはその内部で先頭から1つづつTn::valueを呼んで行き結果が確定したところでインスタンス化と評価を停止することで、インスタンス化の短絡評価を行うためのユーティリティです。

constexpr ifとコンセプトという真新しい言語機能に惑わされてしまっただけで、本質的な問題はTMPの時代から変わっていなかったわけでした・・・。

参考文献

この記事のMarkdownソース