Rokiのチラ裏

学生による学習のログ

Consistent/three-way comparison

先日、米国のニューメキシコ州アルバカーキで開催された ISO C++ 委員会による国際会議にて C++20 に追加された Consistent comparison (p0515) についてのメモ。当エントリー内容は同提案書である p0515r2 に基づく*1。また、同提案の採択と共に導入される Library Support for the Spaceship (Comparison) Operator (p0786r0) も参考としている。

P0515 によって、新しいコア言語機能*2 three-way comparison operator (三方向比較演算子)*3が導入された。これは次の形式をとる。

 lhs \lt=\gt rhs

ドラフトでは次のように記される。

 compare-expression:
 shift-expression
 compare-expression \lt=\gt shift-expression

この式は

  •  lhs \lt rhs である場合、< 0 を比較するオブジェクト( -1)を返す
  •  lhs \gt rhs である場合、> 0 を比較するオブジェクト( +1)を返す
  •  lhs rhs が等価/同等である場合に == 0を比較するオブジェクト( 0)を返す


この時、

  • オペランドの1つが bool type であり、もう一方がそうでない場合は ill-formed である。
  • 両方のオペランドが arithmetic type である場合は、通常の arithmetic conversions がオペランドに適用される。arithmetic conversions で、integral type から floating point type への変換以外の narrowing conversion が必要な場合、ill-formed である。
  • それ以外で、オペランドが integral type である場合、その型の prvalue を返す。

    また、

  • three-way comparison operator はユーザー定義でオーバーロードする事ができ、operator <よりも優先度が高く、<<よりも低い*4

  • 通常リテラル 0 と比較できる型を返すが expression template をサポートするなどの他の戻り型も許可される(言語と標準ライブラリで定義された全てのoperator <=>は、下記で述べる std 名前空間内に定義されるカテゴリ型の1つを返す*5。)
  • fundamental な bool, integral, pointer 型の場合、operator <=> は、strong_ordering を返す。
  • pointer 型の場合様々な cv 修飾と derived から base への変換で同種の組み込みの<=>を呼び出す事ができる。また、組み込みの heterogeneous な operator<=>(T*, nullptr_t)もある。
  • 基本的な floating point 型の場合、operator <=>は、partial_ordering を返し*6、引数をより大きな floating point type に拡大する事によって heterogeneously に呼び出す事ができる。
  • enumeration の場合、operator <=>は enumeration の underlying type の <=>と同じものを返す。同じ値を持つ複数の enumerator がある場合(つまり substitutability が保持されない事を意味する)、型が strong_ordering の場合は weak_ordering に調整され、strong_equality の場合は weak_equality に調整される。
  • nullptr_tの場合、operator <=>は、strong_ordering を返し*7、常に等しくなる。
  • コピー可能な配列T[N](すなわち、非静的データメンバ)の場合、T[N] <=> T[N]Tと同じ<=>による型を返す。また、辞書式に要素間の比較を実行する。他の配列の場合、コピーできないため、operator <=>はない*8
  • void 型はオブジェクトタイプとなれないため、void に対するoperator <=>はない。


three-way comparison operator を利用すると、任意の型の全ての比較を明快に記述する事ができる。そのためには、以下で示されているように、適切な比較のためのカテゴリ型を返すoperator <=>を定義する。

  • 任意の型が演算子  \lt をサポートしているのであれば、_orderingを返し、<,>,<=,>=,==,!=を効率的に生成する。そうでなければ、_equalityを返し、==!=を効率的に生成する。
  • 任意の型a == bf(a) == f(b)*9を意味する*10のであれば、strongを返す。そうでなければ、weakを返す。


この関係性は以下のように表す事ができる。

comparison category types

a == bf(a) == f(b)
表す  a_{0}strong
表さない  a_{1}weak
 +
a < b がサポートされて
いる  b_{0}_ordering
いない  b_{1}_equality
 \downarrow
  •  a_{0} + b_{0} \rightarrowstrong_ordering
  •  a_{0} + b_{1} \rightarrowstrong_equality
  •  a_{1} + b_{0} \rightarrowweak_ordering
  •  a_{1} + b_{1} \rightarrowweak_equality


更に、ordering されていない結果を追加で許可するpartial_orderingもサポートされており、これを返す事もできる*11。これらは全てstd名前空間下にカテゴリ型として定義されており、<compare>ヘッダーをインクルードする事によって利用する事ができる*12。下図のように各カテゴリは一定の is-a 関係、すなわち暗黙変換可能な関係を持っている。p0515r2 に良い図があるので、拝借させて頂く。

f:id:Rok1:20171126213023p:plain

それぞれには予め定義された値があり、それぞれの _order に三つの数値、 _equality には二つの数値がある。さらに partial_ordering は数値とは別に順序付けされていない値を表す事ができる。

category -1 0 +1 Non-numeric values
strong_ordering less equal greater -
weak_ordering less equivalent greater -
partial_ordering less equivalent greater unordered
strong_equality equal nonequal -
weak_equality equivalent nonequivalent -

それぞれ以下のように暗黙的に変換される。

値が {less, equal, greater} の strong_ordering 値が {less, equivalent, greater} の weak_ordering 値が {less, equivalent, greater, unordered} の partial_ordering 値が {equal, unequal} の strong_equality
値が {less, equivalent, greater} の weak_ordering(つまり、同じ値を保持する) 値が {less, equivalent, greater} の partial_ordering(つまり、同じ値を保持する) 値が {nonequivalent, greater, unordered} の weak_equality(つまり、unordered または abs() を適用) 値が {equivalent, nonequivalent} の weak_equality(つまり、同じ値を保持する)
値が {less, equivalent, greater} の partial_ordering(つまり、同じ値を保持する) 値が {nonequivalent, equivalent, nonequivalent} の weak_equality(つまり、abs() を適用) - -
値が {unequal, equal, unequal} の strong_equality(つまり、abs() を適用) - - -
値が {nonequivalent, equivalent, nonequivalent} の weak_equality(つまり、abs() を適用) - - -

これらを踏まえた簡単見分け表は以下のようになる。(p0515r0/5.1.2 参照)

To model this Write operator <=> that returns a And we'll generate  a@b as  a \lt=\gt b @ 0 for
Total order std::strong_ordering < > <= >= == !=
Weak order std::weak_ordering < > <= >= == !=
Partial order std::partial_ordering < > <= >= == !=
Equality comparable std::strong_equality == !=
Equivalence comparable std::weak_equality == !=

ここまでのまとめ

[expr.spaceship] がよくまとまっているのでそれの和訳 + 補足。

  • lhs <=> rhs という形式をとる。
  • 両方のオペランドが arithmetic type である場合、通常の arithmetic conversion ([expr.arith.conv]) がオペランドに適用される。その後、
    • integral type から floating point type への変換以外の narrowing conversion([dcl.init.list])が必要となる場合、ill-formed である。
    • そうでなく、オペランドが integral type である場合、結果はstd::strong_ordering型となる。第一オペランド lhs 、第二オペランド rhs とした時、両方のオペランドが算術的に等しい場合は、std::strong_ordering::equal、第一オペランドが第二オペランドよりも算術的に小さい場合( lhs \lt rhs)はstd::strong_ordering::less、それ以外の場合はstd::strong_ordering::greaterを返す。
    • そうでなく、オペランドが floating point である場合、結果はstd::partial_ordering型となる。式a<=>bは、a が b より小さい場合はstd::partial_ordering::less、b より大きい場合はstd::partial_ordering::greater、a が b と等価であるならば、std::partial_ordering::equivalent、そうでなければstd::partial_ordering::unorderedを返す。
  • 両方のオペランドが同じ enumeration type である場合、オペレータはオペランドの underlying type の型に変換し<=>を変換されたオペランドに適用した結果を返す
  • オペランドの少なくとも1つが pointer 型、array-to-pointer conversion([conv.array])、pointer conversions ([conv.ptr])、function pointer conversions([conv.fctptr])、および修飾変換([conv.qual])は両方のオペランドに対して実行され、それらを複合ポインタ型(composite pointer type)([expr.type])にする。オペランドの少なくとも1つが pointer-to-member 型である場合、メンバへのポインタへの変換([conv.mem])と修飾変換([conv.equal])が両方のオペランドで実行され、複合ポインタ型(composite pointer type)([expr.type])にする。両方のオペランドが null pointer 定数であるが、両方とも integer 型でない場合、pointer conversions([conv.ptr])は両方のオペランドで実行され、それらを複合ポインタ型(composite pointer type)([expr.type])にする。全てのケースで変換後、オペランドらは同じ型でなければならない。尚、両方のオペランドが配列である場合、配列からポインタへの変換([conv.array])は適用されない。
  • 複合ポインタ型が関数ポインタ型、ポインタへのポインタ型、またはstd::nullptr_tの場合、結果はstd::strong_equality型である。結果は、オペランドが equal([expr.eq]) を比較した場合std::strong_equality::equal、unequal を比較した場合はstd::strong_equality::unequal、そうでない場合、unspecified である。
  • 複合ポインタ型がオブジェクトポインタ型の場合、p<=>qstd::strong_ordering型である。2つのポインタオペランドpqが equal ([exptr.eq])を比較するとp<=>qstd::strong_ordering::equalを返し、pqが等しくない場合、p<=>qqpより大きい場合はstd::strong_ordering::lessを返し、pqより大きい場合([expr.rel])はstd::strong_ordering::greaterを返す。それ以外の場合、unspecified である。
  • それ以外の場合、プログラムは ill-formed である。
  • 5つの比較カテゴリ型([cmp.categories])(型 std::strong_orderingstd::strong_equalitystd::weak_orderingstd::weak_equalitystd::partial_ordering)は predefined ではない。そのようなクラス型を使用する前に<compare>ヘッダーが含まれていない場合 -- 型が指定されていない暗黙的な使用(例えば、auto specifier を([dcl.spec.auto])介したデフォルトの three-way comparison([class.spaceship])や、ビルトイン演算子の使用) -- は ill-formed である。
  • 既に比較をサポートしている各標準ライブラリ型に対して適切な比較カテゴリタイプを返す非メンバoperator <=>比較が提供される。これは、既に指定された演算子で一貫した結果を得る事ができるよう提供される*13

Generating two-way comparisons: Rewrite

コンパイラ生成のoperator <=>を除いて、比較式  a @ b ( @は比較演算子)の場合、 a@b,  a \lt=\gt b および  b \lt=\gt a の名前探索を実行する。potential candidate <=>が見つかった場合、次のいずれかが真であれば、それを overload resolution に含める。

  • <=>std::*_ordering を返し、 @==, !=, <, >, <=, >= のうちのいずれかである。
  • <=>std::*_equalityを返し、 @==, !=のうちのいずれかである。

    次に、通常の overload resolution rules に基づいて最適な一致を選択する。ただし、 a @ b,  a \lt=\gt b および/または  b \lt=\gt aが曖昧である場合、最終的に a @ b より  a \lt=\gt b a \lt=\gt b より  b \lt=\gt a といった優先順位になる。つまり、

  •  a \lt=\gt b が best match である場合、 a @ b a \lt=\gt b @ 0 に書き換える。

  •  b \lt=\gt a が best match である場合、 a @ b 0 @ b \lt=\gt a に書き換える。

= default

membership 比較の記述を省略するために、ユーザー宣言された比較演算子はデフォルトで=defaultを使用して以下のセマンティックスで memberwise 比較を選択できる。

  • 関数が<=>の場合、パラメータの型は同じでなければならない。また、戻り値の型はstd:: comparison typesのいずれかでなければならない。また、デフォルトの本体は T の基本(left-to-right depth-first)サブメンバとメンバ(宣言順)サブオブジェクトを連続的に比較して<=>を計算し、以下のように等しくない結果が見つかると早期に停止する。
if (auto cmp = lhs.o <=> rhs.o; cmp != 0) return cmp;
return strong_ordering::equal;
  • 関数が6つの比較演算子の場合、パラメータの型は同じでなければならず、戻り値の型は bool でなければならず、デフォルトの本体は Generating two-way comparisons: Rewrite で述べた規則で呼び出され、対応する<=>を呼び出す。

class template common_comparison_category

common_comparison_category は、全てのテンプレート引数を変換できる最も強い比較カテゴリのエイリアスを提供する。

template <class... Ts>
struct common_comparison_category {
    using type = ...
};

common_comparison_category_t は、Tsから次のように計算される型  C である。

  • Ts が空の時、 Cstrong_orderingである。
  • そのほかの場合、各  T_{i} が戻り型  Cmp_{i} をサポートする場合、 C は全ての  Cmp_{i} から変換できる最も強いカテゴリ型である。
  • そうでない場合、 Cvoidである。

Comparison algorithms

[cmp.alg] を参照。

template<class T> constexpr strong_ordering strong_order(const T& a, const T& b);

二つの値を比較し、strong_ordering 型の結果を生成する。

  • もしnumeric_limits<T>::is_iec559が true の場合、ISO/IEC/IEEE 60559 で指定されている totalOrder 操作と一貫性のある型の結果を返す
  • それ以外の場合、その式が well-formed であり、strong_ordering に変換可能であれば a<=>bを返す。
  • それ以外の場合、式a <=> bが well-formed である場合、関数は削除されたものとして定義される。
  • それ以外の場合、式a == ba < bがそれぞれ well-formed で bool に変換可能ならば
    • もしa == bが true なら strong_ordering::equalを返す
    • それ以外の場合、a < bが true なら、strong_ordering::lessを返す
    • それ以外の場合、strong_ordering::greaterを返す
  • それ以外の場合、関数は削除されたものとして定義される。

template<class T> constexpr weak_ordering weak_order(const T& a, const T& b);

二つの値を比較し、weak_ordering 型の結果を生成する。

  • 式が well-formed で weak_ordering に変換可能であれば、a <=> bを返す
  • それ以外の場合、式a <=> bが well-formed である場合、関数は削除されたものとして定義される。
  • それ以外の場合、式a == ba < bがそれぞれ well-formed で bool に変換可能ならば
    • もしa == bが true なら weak_ordering::equivalentを返す
    • それ以外の場合、a < bが true なら weak_ordering::less を返す
    • それ以外の場合、weak_ordering::greaterを返す
  • それ以外の場合、関数は削除されたものとして定義される。

template<class T> constexpr partial_ordering partial_order(const T& a, const T& b);

二つの値を比較し、partial_ordering 型の結果を生成する。

  • 式が well-formed で partial_ordering に変換可能であれば、a <=> bを返す。
  • それ以外の場合、式a <=> bが well-formed である場合、関数は削除されたものとして定義される。
  • それ以外の場合、式a == ba < bがそれぞれ well-formed で bool に変換可能ならば
    • もしa == bが true なら partial_ordering::equivalentを返す
    • それ以外の場合、a < bが true なら partial_ordering::lessを返す
    • それ以外の場合、partial_ordering::greaterを返す
  • それ以外の場合、関数は削除されたものとして定義される。

template<class T> constexpr strong_equality strong_equal(const T& a, const T& b);

二つの値を比較し、strong_equality 型の結果を生成する。

  • 式が well-formed で strong_equality に変換可能であれば、a <=> bを返す。
  • それ以外の場合、式a <=> bが well-formed である場合、関数は削除されたものとして定義される。
  • それ以外の場合、式a == bが well-formed で bool に変換可能ならば
    • もしa == bが true なら strong_equality::equalを返す
    • それ以外の場合、strong_equality::nonequalを返す
  • それ以外の場合、関数は削除されたものとして定義される。

template<class T> constexpr weak_equality weak_equal(const T& a, const T& b);

二つの値を比較し、weak_equality 型の結果を生成する。

  • 式が well-formed で weak_equality に変換可能であれば、a <=> bを返す。
  • それ以外の場合、式a <=> bが well-formed である場合、関数は削除されたものとして定義される。
  • それ以外の場合、式a == bが well-formed で bool に変換可能ならば
    • a == bが true ならweak_equality::equivalentを返す。
    • それ以外の場合、weak_equality::nonequivalentを返す。
  • それ以外の場合、関数は削除されたものとして定義される。

    次に [alg.3way] を参照。
template<class T, class U> constexpr auto compare_3way(const T& a, const U& b);

二つの値を比較し、最も強い適用可能な比較カテゴリ型を戻す。

  • a <=> bが well-formed である場合、a <=> bを返す。
  • それ以外の場合、a == bまたa < bが well-formed で bool に変換可能な場合、a == bが true である場合、std::strong_ordering::equal、そうでない場合、a < bが true である場合、std::strong_ordering::less、そうでない場合、std::strong_ordering::greaterを返す。
  • それ以外の場合、a == b が well-formed で bool に変換可能な場合、a == bが true である場合std::strong_equality::equal、そうでない場合、std::strong_equality::none_equalを返す。
  • それ以外の場合、関数は削除されたものとして定義される。

template<class InputIterator1, class InputIterator2, class Cmp>
  constexpr auto
    lexicographical_compare_3way(InputIterator1 b1, InputIterator1 e1,
                                 InputIterator2 b2, InputIterator2 e2,
                                 Cmp comp)
      -> common_comparison_category_t<decltype(comp(*b1, *b2)), strong_ordering>;
  • Cmpは戻り型が比較カテゴリ型の関数オブジェクト型とする。
  • 辞書学的に 2 つの範囲を比較し、最も強力な適用可能な比較カテゴリの結果を生成する。効果は以下の通りである。
for ( ; b1 != e1 && b2 != e2; void(++b1), void(++b2) )
  if (auto cmp = comp(*b1,*b2); cmp != 0)
    return cmp;
return b1 != e1 ? strong_ordering::greater :
       b2 != e2 ? strong_ordering::less :
                  strong_ordering::equal;

また次のオーバーロードも定義される。

template<class InputIterator1, class InputIterator2>
  constexpr auto
    lexicographical_compare_3way(InputIterator1 b1, InputIterator1 e1,
                                 InputIterator2 b2, InputIterator2 e2);

効果は次の通りである。

return lexicographical_compare_3way(b1, e1, b2, e2,
                                    [](const auto& t, const auto& u) {
                                      return compare_3way(t, u);
                                    });

strong ordered type の例

完全に ordering 付けられた memberwise*14の比較を取得するためには、std::strong_orderingを返す。以下のように記述できる。

class Point {
    int x; int y;
public:
    friend std::strong_ordering operator<=>(const Point&, const Point&) = default;
};

これによってPointは、全ての比較をサポートした事となる。<=>を一度呼び出す事によって、全ての比較をあたかも*15別の実際の関数を作成せずに効率的に実装する。

Point p1, pt2;
if (pt1 == pt2) { /* ... */ } // ok

set<Point> s; // ok
s.insert(pt1); // ok

if (pt1 <= pt2) { /* ... */ } // ok, single call to <=>

非 memberwise に対する ordering は、= defaultの替わりに、独自の定義を作成する*16 *17 *18

class TotallyOrdered : Base {
    string tax_id;
    string first_name;
    string last_name;
public:
    std::strong_ordering operator<=>(const TotallyOrdered& that) const {
        if (auto cmp = (Base&)(*this) <=> (Base&)that; cmp != 0) return cmp;
        if (auto cmp = last_name <=> that.last_name; cmp != 0) return cmp;
        if (auto cmp = first_name <=> that.first_name; cmp != 0) return cmp;
        return tax_id <=> that.tax_id;
    }
};

この提案によって、TotallyOrderedを使用するコードは、完全に ordering された三つの比較を含む全ての比較を実行する事ができ、<=>一つの呼び出しで効率的に実装される(as-if)。

TotallyOrdered to1, to2;
if (to1 == to2) { /*...*/ } // ok
set<TotallyOrdered> s; // ok
s.insert(to1); // ok
if (to1 <= to2) { /*...*/ } // ok, single call to <=>

weakly ordered type の例

weak ordering を定義するためにはstd::weak_orderingを返す。weak ordering を定義したい場面として、例えば以下のように大文字小文字を区別しないような文字列比較が考えられる。これにはメンバの一つをメンバ自身の持つ比較とは異なる方法で比較を行うために、= defaultではなく、独自のカスタム比較を使用する。

class CaseInsensitiveString {
    string s;
public:
    friend std::weak_ordering operator<=>(const CaseInsensitiveString& a, const CaseInsensitiveString& b) {
        return case_insensitive_comare(a.s.c_str(), b.s.c_str());
    }
};

この提案によって、CaseInsensitiveStringを使用するコードは weak ordering の three-way comparison を含む全ての比較を実行でき、<=>一つの呼び出しで効率的に実装される(as-if)。

CaseInsensitiveString cis1, cis2;
if (cis1 == cis2) { /*...*/ } // ok
set<CaseInsensitiveString> s; // ok
s.insert(/*...*/); // ok
if (cis1 <= cis2) { /*...*/ } // ok, performs one comparison operation

さらに、C スタイルのchar*文字列との symmetric heterogeneous comparison*19を提供するには以下のように記述できる。

// add this function in CaseInsensitiveString
 friend std::weak_ordering operator<=>(const CaseInsensitiveString& a, const char* b) {
     return case_insensitive_compare(a.s.c_str(), b);
 }

この提案によって、CaseInsensitiveStringを使用するコードは全ての文字列 string/char* および char*/string で比較を実行でき、<=および他の比較演算子は単一の<=>を使用して効率的に実装される。

Partially ordered type の例

partially ordered なクラスはstd::partial_orderingを返すoperator <=>を定義する事で、コンパイラ生成の比較として全ての双方向の比較を取得 する事ができる。結果は、2つのオブジェクト間に順序関係がない(unordered)である事を表す事ができ、その場合、双方向比較の全てがfalseを返す。

class PersonInFamilyTree {
public:
    std::partial_ordering operator<=>(const PersonInFamilyTree& that) const {
        if (this->is_the_same_person_as(that)) return partial_ordering::equivalent;
        if (this->is_transitive_child_of(that)) return partial_ordering::less;
        if (that.is_transitive_child_of(*this)) return partial_ordering::greater;
        return partial_ordering::unordered;
    }
};

この提案によって、 PersonInFamilyTreeは全ての比較を実行する事ができる。

PersonInFamilyTree per1, per2;
 if (per1 == per2) { /*...*/ } // ok, per1 is per2
else if (per1 < per2) { /*...*/ } // ok, per2 is an ancestor of per1
else if (per1 <= per2) { /*...*/ } // ok, per2 is per1 or an ancestor of per1
else if (per1 > per2) { /*...*/ } // ok, per1 is an ancestor of per2
else if (per1 >= per2) { /*...*/ } // ok, per1 is per2 or an ancestor of per2
else { /*...*/ } // per1 and per2 are unrelated
 if (per1 != per2) { /*...*/ } // ok, per1 is not per2

Equality comparable type の例

equality compare ができ、カスタマイズされた順序付けのあるクラスはstd::strong_equalityを返すoperator <=>を定義しコンパイラ生成の比較として==および!=を取得する事ができる。例えば

class EqualityComparable {
    string name;
    BigInt number1;
    BigInt number2;
public:
    std::strong_equality operator<=>(const EqualityComparable& that) const {
        if (auto cmp = number1 <=> that.number1; cmp != 0) return cmp;
        if (auto cmp = number2 <=> that.number2; cmp != 0) return cmp;
        return name <=> that.name;
    }
};

この提案によって、EqualityComparable==!=を実行する事ができる。

Equivalence comparable type の例

equivalence compare ができ、カスタマイズされた順序付けのあるクラスはstd::weak_equalityを返すoperator<=>を定義しコンパイラ生成の比較として==および!=を取得する事ができる。例えば

class EquivalenceComparable {
    CaseInsensitiveString name;
    BigInt number1;
    BigInt number2;
public:
    std::weak_equality operator<=>(const EquivalenceComparable& that) const {
        if (auto cmp = number1 <=> that.number1; cmp != 0) return cmp;
        if (auto cmp = number2 <=> that.number2; cmp != 0) return cmp;
        return name <=> that.name;
    }
};

この提案によって、EquivalenceComparable==!=を実行する事ができる。

尚この提案が C++20 に導入されるにあたって、std::rel_ops::rel_opsは deprecated とされる。似たような機能を持つ Boost Operators Library も今後の使用は避けた方が良いだろう。また、以下のようなoperator<=のアドレスをテンプレートパラメータやoperator >の左辺オペランドに適用するコードは C++20 以降互換性のないものとなる。解決策としては下記のようにスペースを入れる必要がある*20

X<&Y::operator<=>(); // C++20: NG
x + &operator<=>y; // C++20: NG

X<&Y::operator<= >(); // OK
x + &operator<= >y; // OK

別のホワイトペーパー P0790R0 ではoperator <=>が与える影響と、既存の標準ライブラリの対応がリストアップされている。

*1:同提案のデザイン原則として、Herb Sutter 氏定番の三つの原則が掲げられている。これについては P0707R1 Metaclasses の Design principlesを参照。

*2:これはコア言語機能ではあるが、この新しい演算子の動作は comparison category type と呼ばれる新しい標準ライブラリコンポーネントに依存する形となる。

*3:口語では spaceship operator の名でよく知られている

*4:例えば同じ優先順位だった場合、@ が任意の比較演算子である時、a <=> b @ 0 は、正しく評価されるが、0 @ a <=> b を書くと <=> が矛盾した動きをする事となる。@ が {==, !=} の時は最初に評価されるが、@ が {<, >, <=, >=} の時は二番目に評価される事となる。a<=> b @ 0 と 0 @ a <=> b を一貫させるには <=> は < より少し優先度が高いはずである。

*5:尚、operator <=> の実装の外側にあるユーザーコードでは <=> を直接呼び出すべきではない。あくまでもライブラリや特定の型の実装で利用するべきであり、例えば a < b を ユーザランドで a <=> b < 0と記述するべきではない。

*6:符号付き 0 と NaN の両方をサポートするために partial_ordering を使用する。-0 <=> +0 は equival を返し、NaN <=> anything は何も返さない。

*7:常に equal を返すため、単に strong_equality を返す事もできるが、strong_ordering を返す事でより多くのコンテキストで使用できる。

*8:配列の場合、コピーと比較の一貫性を維持するために配列が言語によってコピーできない場合は比較を行わない。配列同士の場合、配列からポインタへの変換は適用されないため、arr1 <=> arr2 は ill-formed である事に注意

*9:f は 非 private な const インタフェースを使用してアクセス可能な comparison-salient の状態を読み込む

*10:substitutability

*11:つまり、strong ordering, weak ordering は六つの比較演算子を全てを許可し、x < y, x == y, x > y のどれかが true でなければならないが、partial ordering は六つの比較演算子の全てを許可するが、x < y, x == y, x > y のどれも true である必要はない。strong ordering, weak ordering の特徴はTrichotomy のままである。

*12:[expr.spaceship]/10

*13:コンテナ(文字列含む)、string_view, optional, any, unique_ptr 及び shared_ptr などのいくつかの std:: 型はカスタムセマンティックスを取得するためにカスタム演算子 <=> を宣言して定義する。特に、string と string_view は strcmp と同等のものが得られ、strcmp とは異なり組み込みの null が尊重される点で優れている点に留意。また、complex といった 一部の std:: 型で <=> は strong_equality などの弱い比較カテゴリを返す。

*14:各基本またはメンバのサブオブジェクトについての略語であると本文にて述べられている

*15:pt1 == pt2 の場合、quality implementation が内部の2つの int メンバに対して == を呼び出す事が期待されるため、ここでは as-if と述べられている。

*16:メンバが strong_ordering を持たない場合、該当コードはコンパイル時にエラーを発生させる。これによって、比較のセマンティックスと実際の実装が異なっていたとしても、コンパイル時に捕らえる事ができる

*17:ユーザー定義の operator <=> が total order である事を保証する最も効果的な方法は、全てのデータメンバ(基底)を明示的に比較する事である。データメンバを余らせると、a == b が f(a) == f(b) であるという substitutability を提供できなくなるリスクがある。

*18:ここで memberwise <=> と 0 を比較する事は、個々のデータメンバの比較カテゴリを agnostic にするためのもの(比較カテゴリは変化する可能性がある)であり、意図的なものである。

*19:対称異種比較と言ったところか。weak ordering の定義そのままである

*20:本文によれば、唯一の後方ソースコードに対する非互換性であるとのこと。また、このようなコードをスペースなしで動作させるための特別な構文解析ルールを採用する事もできるが、利益があまりにも少ないとのこと。