[C++]expression-equivalentのお気持ち

expression-equivalent??

標準ライブラリへのRangeの導入に伴って新たに追加された言葉で、次のように定義されています。

expressions that all have the same effects, either are all potentially-throwing ([except.spec]) or are all not potentially-throwing, and either are all constant subexpressions or are all not constant subexpressions

何となく噛み砕くと以下のような意味合いです

ある2つ以上の式は、次の全てを満たす場合にexpression-equivalentである

  • 式は同じ効果を持つ
  • 例外を投げるかどうかが同一
    • 全ての式は例外を投げない
    • もしくは、全ての式は例外を投げうる
  • 式が定数式で実行可能であるかも同一
    • 全ての式は、(部分式としても)定数実行可能である
    • もしくは、全ての式は(部分式としても)定数実行不可

これは要するに、式の効果と例外を投げるかどうか、および定数式で実行可能かどうか、が全く同一である時にexpression-equivalentの関係にある、という事です。

これは何?

これは主にRangeライブラリのカスタマイぜーションポイントオブジェクト(以下CPO)の効果の定義において頻出します。
大体以下の様な形式で書かれています。

The expression CPO-name(E) for some subexpression E is expression-equivalent to:

  • expression-equivalentとなる式 if 条件
  • Otherwise, expression-equivalentとなる式 if 条件
  • ...
  • Otherwise, CPO-name(E) is ill-formed.

ここでのCPO-nameは任意のカスタマイぜーションポイントオブジェクト名で、EとはそのCPOの呼び出しに引数として与えられている式のことです。
そしてこの文章は、引数EによってCPO-name(E)の呼び出しがどのような効果を持つか?をつらつらと書いています(大体最後はill-formedとなりますが、ならない場合もあります)。

これは、これまでの標準ライブラリ関数等ならばその効果(Effects)の定義において、Equivalent to :以下に書かれていたものです。
つまり、expression-equivalentはこれまで説明に使われていたEquivalent toをCPO用に置き換えているものだと言えます。

Equivalent toとの違い

Equivalent toでは、ある関数等の効果を別の式の効果と等価であるとして定義します。この時、その効果には式が例外を投げるのかどうか、また部分的にでも定数式で実行可能であるか、が含まれてはいないようです。
それらは式の効果ではなく、関数に指定されているものだからです。

Rangeライブラリのカスタマイゼーションポイントオブジェクトは名前空間スコープに定義された関数オブジェクトであり、その呼び出しではADL等の機構によりユーザーが定義した任意の型に対してさえも目的となる処理を行おうとします。 その結果として、CPOの効果は一通りではありません。

Rangeライブラリ利用ユーザーはCPOの持つ効果のどれかに引っかかるように巧妙に自分の持つ型をカスタマイズすれば、Rangeライブラリに定義されている処理に任意の型をアダプトできます。

効果が複数あり、しかも入ってくる型がどのようにその効果のいずれかに対してアダプトされているかはわかりません。そのため、CPOの呼び出しは定数実行できるのか?呼び出しに伴って例外を投げるのか?は実行される処理によります。

Equivalent toでは指定した式のconstexpr性及びnoexcept性は伝播されないので、Equivalent toで効果を指定するだけではCPOの呼び出しがconstexprであるかnoexceptであるかは未規定になってしまいます。

そのため、expression-equivalentが必要になります。expression-equivalentな関係にある2つの式は、効果だけではなくconstexpr性及びnoexcept性に関しても同一です。
従って、CPOの呼び出しの効果としてexpression-equivalentとされている式に呼び出された型を当てはめることで、その効果と定数実行可能であるか?例外を投げうるか?をも含めて表明することができます。

なにいってんだこいつという感じなので例として、std::ranges::begin()を見てみましょう。
std::begin()は関数ですが、これはカスタマイゼーションポイントオブジェクトです。その効果は次のようにあります。

The name ranges​::​begin denotes a customization point object. The expression ranges​::​​begin(E) for some subexpression E is expression-equivalent to:

  • E + 0 if E is an lvalue of array type ([basic.compound]).
  • Otherwise, if E is an lvalue, decay-copy(E.begin()) if it is a valid expression and its type I models input_­or_­output_­iterator.
  • Otherwise, decay-copy(begin(E)) if it is a valid expression and its type I models input_­or_­output_­iterator with overload resolution performed in a context that includes the declarations: template<class T> void begin(T&&) = delete;
    template<class T> void begin(initializer_list<T>&&) = delete;
    and does not include a declaration of ranges​::​begin.
  • Otherwise, ranges​::​begin(E) is ill-formed.

引数の式をEとしてstd::ranges::begin(E)のように呼び出したとき、その効果はEに応じてその下に書かれている4つのいずれかと等価(expression-equivalent)、という事を言っています。

ユーザーが自作する任意の型に対して作用するのはおそらく2つ目と3つ目のものです。それぞれ以下のように定義されます。

  • 2つ目は、引数Eの(結果となるオブジェクトの)メンバ関数として定義されているE.begin()を呼び出す。
  • 3つ目は、Eの(結果となるオブジェクトの)関連名前空間からADLで見つかるbegin()か、std::begin()を呼び出す。

このbegin()をアダプトしたつもりの自作の型Tのオブジェクトaで呼び出したときにexpression-equivalentであるとは

2つ目の形式にアダプトした場合、std::ranges::begin(a)の呼び出しがconstexprとなるかは(あなたが書いた)a.begin()の定義によって決まり、例外を投げるかも(あなたが書いた)a.begin()によって決まるという事です。

同様に、3つ目の形式にアダプトした場合も、ユーザーが定義した(あなたが書いた)フリー関数のbegin(a)constexprなのかnoexceptなのかでそれらが決定される訳です。

さらに、どちらの場合も結果となるイテレータdecay_copyされて返されますが、このdecay_copyの処理が同様にconstexprなのかnoexceptなのかも(おそらくあなたが定義しているであろう)返されたイテレータ型によるわけです。

つまりはとっても他力本願な定義の仕方なのです。

参考文献

謝辞

この記事の9割は以下の方によるご指摘によって成り立っています。

この記事のMarkdownソース