C++20:三方比較演算子

3 者間比較演算子 <=> は、単に宇宙船演算子と呼ばれることがよくあります。 spaceship オペレーターは、2 つの値 A と B について、A B のいずれであるかを判別します。spaceship オペレーターを定義するか、コンパイラーが自動生成することができます。

3 者間比較演算子の利点を理解するために、古典的な話を始めましょう。

C++20 より前の順序

シンプルな int ラッパー MyInt を実装しました。もちろん、MyInt を比較したいです。 isLessThan 関数テンプレートを使用した私のソリューションは次のとおりです。

// comparisonOperator.cpp

#include <iostream>

struct MyInt {
 int value;
 explicit MyInt(int val): value{val} { }
 bool operator < (const MyInt& rhs) const { 
 return value < rhs.value;
 }
};

template <typename T>
constexpr bool isLessThan(const T& lhs, const T& rhs) {
 return lhs < rhs;
}

int main() {

 std::cout << std::boolalpha << std::endl;

 MyInt myInt2011(2011);
 MyInt myInt2014(2014);

 std::cout << "isLessThan(myInt2011, myInt2014): "
 << isLessThan(myInt2011, myInt2014) << std::endl;

 std::cout << std::endl;

}

プログラムは期待どおりに動作します:

正直なところ、MyInt は直感的でない型です。 6 つの順序関係の 1 つを定義するときは、それらすべてを定義する必要があります。直観的な型は、少なくとも半規則的であるべきです:「C++20:規則規則と規則規則の概念を定義してください。」

さて、定型コードをたくさん書かなければなりません。不足している 5 つの演算子は次のとおりです。

bool operator==(const MyInt& rhs) const { 
 return value == rhs.value; 
}
bool operator!=(const MyInt& rhs) const { 
 return !(*this == rhs); 
}
bool operator<=(const MyInt& rhs) const { 
 return !(rhs < *this); 
}
bool operator>(const MyInt& rhs) const { 
 return rhs < *this; 
}
bool operator>=(const MyInt& rhs) const { 
 return !(*this < rhs); 
}

終わり?いいえ! MyInt と int を比較したいと思います。 int と MyInt、および MyInt と int の比較をサポートするには、コンストラクターが明示的に宣言されているため、各演算子を 3 回オーバーロードする必要があります。明示的なおかげで、int から MyInt への暗黙的な変換は開始されません。便宜上、演算子をクラスのフレンドにします。私の設計上の決定に関する背景情報がさらに必要な場合は、私の以前の投稿「C++ コア ガイドライン:オーバーロードとオーバーロード演算子の規則」をお読みください。

これらは、small-than の 3 つのオーバーロードです。

friend bool operator < (const MyInts& lhs, const MyInt& rhs) { 
 return lhs.value < rhs.value;
}

friend bool operator < (int lhs, const MyInt& rhs) { 
 return lhs < rhs.value;
}

friend bool operator < (const MyInts& lhs, int rhs) { 
 return lhs.value < rhs;
}

これは、合計で 18 個の比較演算子を実装する必要があることを意味します。これで話は終わりですか? MyInt とすべての演算子を constexpr にする必要があると判断したためです。演算子を noexcept にすることも検討する必要があります。

これは三方比較演算子にとって十分な動機であると思います.

C++20 での注文

3 方向比較演算子を定義するか、=default を使用してコンパイラに要求することができます。どちらの場合も、==、!=、<、<=、>、および>=の 6 つの比較演算子をすべて取得します。

// threeWayComparison.cpp

#include <compare>
#include <iostream>

struct MyInt {
 int value;
 explicit MyInt(int val): value{val} { }
 auto operator<=>(const MyInt& rhs) const { // (1) 
 return value <=> rhs.value;
 }
};

struct MyDouble {
 double value;
 explicit constexpr MyDouble(double val): value{val} { }
 auto operator<=>(const MyDouble&) const = default; // (2)
};

template <typename T>
constexpr bool isLessThan(const T& lhs, const T& rhs) {
 return lhs < rhs;
}

int main() {
 
 std::cout << std::boolalpha << std::endl;
 
 MyInt myInt1(2011);
 MyInt myInt2(2014);
 
 std::cout << "isLessThan(myInt1, myInt2): "
 << isLessThan(myInt1, myInt2) << std::endl;
 
 MyDouble myDouble1(2011);
 MyDouble myDouble2(2014);
 
 std::cout << "isLessThan(myDouble1, myDouble2): "
 << isLessThan(myDouble1, myDouble2) << std::endl; 
 
 std::cout << std::endl;
 
}

ユーザー定義 (1) およびコンパイラ生成 (2) の 3 方向比較演算子は期待どおりに機能します。

しかし、この場合にはいくつかの微妙な違いがあります。 MyInt (1) のコンパイラによって推定される戻り値の型は強い順序付けをサポートし、コンパイラによって推定される MyDouble の戻り値の型は部分的な順序付けをサポートします。 NaN (非数) などの浮動小数点値は順序付けできないため、浮動ポインタ数は部分的な順序付けのみをサポートします。たとえば、NaN ==NaN は false です。

ここで、コンパイラによって生成された宇宙船オペレーターに関するこの投稿に焦点を当てたいと思います。

コンパイラ生成の宇宙船オペレーター

コンパイラによって生成された 3 方向比較演算子には、暗黙的な constexpr および noexcept であるヘッダー が必要です。さらに、辞書式比較を実行します。何? constexpr から始めましょう。

コンパイル時の比較

3 者間比較演算子は暗黙の constexpr です。したがって、前のプログラム threeWayComparison.cpp を単純化し、コンパイル時に次のプログラムで MyDouble を比較します。

// threeWayComparisonAtCompileTime.cpp

#include <compare>
#include <iostream>

struct MyDouble {
 double value;
 explicit constexpr MyDouble(double val): value{val} { }
 auto operator<=>(const MyDouble&) const = default; 
};

template <typename T>
constexpr bool isLessThan(const T& lhs, const T& rhs) {
 return lhs < rhs;
}

int main() {
 
 std::cout << std::boolalpha << std::endl;

 
 constexpr MyDouble myDouble1(2011);
 constexpr MyDouble myDouble2(2014);
 
 constexpr bool res = isLessThan(myDouble1, myDouble2); // (1)
 
 std::cout << "isLessThan(myDouble1, myDouble2): "
 << res << std::endl; 
 
 std::cout << std::endl;
 
}

コンパイル時に比較の結果を要求し (1)、それを取得します。

コンパイラによって生成された 3 方向比較演算子は、辞書式比較を実行します。

辞書式比較

この場合、辞書式比較とは、すべての基本クラスが左から右に比較され、クラスのすべての非静的メンバーが宣言順に比較されることを意味します。修飾する必要があります:パフォーマンス上の理由から、コンパイラによって生成された ==および !=演算子は、C++20 では異なる動作をします。このルールの例外については、次の投稿で書きます。

記事「ロケット科学でコードを簡素化:C++20 の宇宙船オペレーター」Microsoft C++ チーム ブログでは、辞書式比較の印象的な例を提供しています。

struct Basics {
 int i;
 char c;
 float f;
 double d;
 auto operator<=>(const Basics&) const = default;
};

struct Arrays {
 int ai[1];
 char ac[2];
 float af[3];
 double ad[2][2];
 auto operator<=>(const Arrays&) const = default;
};

struct Bases : Basics, Arrays {
 auto operator<=>(const Bases&) const = default;
};

int main() {
 constexpr Bases a = { { 0, 'c', 1.f, 1. }, // (1)
 { { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } };
 constexpr Bases b = { { 0, 'c', 1.f, 1. }, // (1)
 { { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } };
 static_assert(a == b);
 static_assert(!(a != b));
 static_assert(!(a < b));
 static_assert(a <= b);
 static_assert(!(a > b));
 static_assert(a >= b);
}

プログラムの最も複雑な側面は、宇宙船のオペレーターではなく、集約の初期化 (1) による Base の初期化であると思います。集合体の初期化により、メンバーがすべて public である場合、クラス型 (クラス、構造体、共用体) のメンバーを直接初期化できます。この場合、ブレースの初期化を使用できます。集計の初期化について詳しく知りたい場合は、cppreference.com で詳細を確認できます。集計の初期化については、C++20 での指定された初期化を詳しく見ていく今後の投稿で詳しく書く予定です。

次は?

コンパイラは、すべての演算子を生成するときに非常に賢い仕事をします。最終的に、直感的で効率的な比較演算子を無料で入手できます。次回の投稿では、内部の魔法について深く掘り下げます。