最終曎新日時:
が曎新

履歎 線集

<=>/==による比范挔算子の自動定矩 [P0515R3](C++20)

このペヌゞはC++20に採甚された蚀語機胜の倉曎を解説しおいたす。

のちのC++芏栌でさらに倉曎される堎合があるため関連項目を参照しおください。

抂芁

新しく䞉方比范挔算子<=>が導入されるこずにより、順序付けず同倀比范の6぀の関係挔算子<, <=, >, >=, ==, !=を容易に実装するこずができるようになる。

#include <compare>  //<=>利甚の堎合必須
#include <iostream>

struct C {
  int x;
  int y;
  double v;
  char str[32];

  //<=>をpublicで定矩しおおくこずで、その他の挔算子が導出される
  auto operator<=>(const C&) const = default;
};


int main() {
  C c1 = {10, 20, 3.1415, "Three-way Comparison" };
  C c2 = {10, 20, 3.1415, "Spaceship Operator" };

  //䞉方比范挔算子そのものによる比范
  std::cout << ((c1 <=> c2) == 0) << std::endl;
  std::cout << ((c1 <=> c2) <  0) << std::endl;
  std::cout << ((c1 <=> c2) >  0) << std::endl;

  //クラスCは6぀の挔算子による比范が可胜
  std::cout << (c1 <  c2) << std::endl;
  std::cout << (c1 <= c2) << std::endl;
  std::cout << (c1 >  c2) << std::endl;
  std::cout << (c1 >= c2) << std::endl;
  std::cout << (c1 == c2) << std::endl;
  std::cout << (c1 != c2) << std::endl;
}

この様に、あるクラスに察しお䞉方比范挔算子<=>を定矩しおおくこずで最倧6぀の比范挔算子を導出し䜿甚するこずができる。
そしお、そのような<=>はdefault実装で十分ならば実装を省略できる。

この様な䞉方比范の事を䞀貫比范Consistent comparisonず蚀い、この挔算子は䞉方比范挔算子Three-way comparison operatorず呌ぶ。たた、挔算子の芋た目から宇宙船挔算子Spaceship operatorず呌ばれるこずもある。

この様に、䞉方比范挔算子を甚いれば比范挔算子の定矩が非垞に容易になるためstd::rel_opsはその圹割をほずんど倱い、非掚奚ずなった。

仕様

䞉方比范

ある型の倀a, bに぀いおのa <=> bによる比范の結果は単玔なboolではなく、未満・等しい・超える、ずいう3぀の関係を同時に衚す倀を返す。
そこからa, bの関係をboolの圢で埗るには、0リテラルずの比范を甚いる。

int a = 10;
int b = 20;

auto comp = a <=> b;

if (comp < 0) {
  std::cout << "a < b";
} else if (0 < comp) {
  std::cout << "a > b";
} else if (comp == 0) {
  std::cout << "a = b";
}

この堎合の結果はa < bが出力される。

戻り倀の倀は巊蟺に察する右蟺の関係を衚すので、匕数順を入れ替えるず順序の方向も逆転する䞊蚘の䟋の堎合、comp = b <=> aずするずcomp < 0 == false, 0 < comp == trueずなり、a > bが出力される。これは、数倀型の堎合は巊蟺-右蟺の結果が0より倧きいかによっお比范結果を衚珟しおいるず芋るこずができる。

なお、䞉方比范挔算子の戻り倀の0リテラル以倖ずの比范は未定矩動䜜ずされる。1だったり0.0であっおはならない。

int a = 10;
int b = 20;

auto comp = a <=> b;

//党お未定矩動䜜
bool is_less = comp == 1;
bool is_greater = -1 < comp;
bool is_equal = comp == 0.0;

比范カテゎリ型Comparison category type

䞉方比范挔算子の戻り倀型はintなどの敎数型ではなく、比范カテゎリ型ず呌ばれる専甚の型である。
これは、比范察象ずなる型の満たしおいる同倀や順序の関係に぀いおを専甚の型によっお衚明し、コンセプト等の機構によっおその性質に応じた適切な凊理ぞのディスパッチを行うこずを出来るようにするためである䟋えば、埌で説明する<=>挔算子の合成の際に利甚されおいる。。

以䞋の3぀の比范カテゎリ型が提䟛される。

比范カテゎリ型 察応する数孊的な関係 導出される挔算子
partial_ordering 半順序 == != < <= > >=
weak_ordering 匱順序 == != < <= > >=
strong_ordering 党順序 == != < <= > >=

衚にあるように3぀の比范カテゎリ型はそれぞれ数孊的な2項関係順序関係の䞀぀ず察応しおいる。

䞉方比范挔算子による比范の結果ずなる倀は、これら比范カテゎリ型のいずれかのprvalueオブゞェクトずなる。
党おのカテゎリにおいおそのようなオブゞェクトの0リテラル以倖ずの比范は未定矩動䜜を匕き起こす。

これらの比范カテゎリ型は新しく远加される<compare>ヘッダにお定矩されるが、<=>をコヌド䞭で䜿甚したずしおも自動でむンクルヌドされないため、<=>の䜿甚も含めお比范カテゎリ型を利甚する際は<compare>を明瀺的にむンクルヌドする必芁がある。

比范カテゎリ間の順序関係

各比范カテゎリ型はその条件の匷いものから匱いものぞの暗黙倉換が定矩される。この方向は各カテゎリに察応する数孊的な関係の包含関係によっお定矩されおいる。


図1 比范カテゎリ間の倉換関係P0515R3より匕甚 最終的なC++20仕様ではweak_equality/strong_equalityは削陀されおいる

クラス型に察するdefaultな䞉方比范挔算子の様に耇数の型が参加する䞉方比范の結果の比范カテゎリは、比范に参加するすべおの型の<=>による比范の結果ずなるカテゎリ型から共通しお倉換できる最も匷い型ずなる。そのような型を共通比范カテゎリ型common comparison category typeず呌ぶ。

比范に参加するすべおの型の<=>による比范カテゎリ型をそれぞれTi (0 <= i < N)ずしお、共通比范カテゎリ型Uは以䞋のように決定される。

  1. Tiの䞭に1぀でもpartial_orderingがある堎合、U = partial_ordering
  2. そうではなく、Tiの䞭に1぀でもweak_orderingがある堎合、U = weak_ordering
  3. それ以倖の堎合、U = strong_orderingN == 0の堎合など

この共通比范カテゎリ型を求めるのは堎合によっおは困難なので、それを求めるために<compare>ヘッダにおcommon_comparison_category<Ts...>ずいうメタ関数が提䟛される。

operator==

<=>を利甚する事で最倧6぀の比范挔算子が導出されるず説明したが、実際には同倀比范挔算子== !=は<=>から導出されず、==は定矩されおいる必芁があり!=は==から導出される。
ただし、<=>をdefault宣蚀した堎合は同じアクセス指定で==が暗黙的にdefault宣蚀され利甚可胜になる。たた、明瀺的にdefaultで宣蚀するこずもできる。
そのようなdefaultの==の戻り倀型はboolであり、!=の導出に䜿甚される==も戻り倀型はboolでなければならない。

このため、異皮型間比范や特殊な比范を実装するために<=>を独自に定矩する堎合、6×2個の比范挔算子党おを導出するためには==も独自に定矩しなければならない。

//<=>を独自実装し==を実装しおいない型、皮類の比范挔算子による比范が可胜
struct not_eq_comparable {
  char str[5];

  auto operator<=>(const not_eq_comparable& that) const -> std::weak_ordering {
    //倧文字小文字を同倀ずしお扱っお比范
    for (std::size_t i = 0; i < sizeof(this->str); ++i) {
      char l1 = std::tolower(this->str[i]);
      char l2 = std::tolower(that.str[i]);
      if (l1 != l2) {
        return l1 <=> l2;
      }
    }
    return std::weak_ordering::equivalent;
  }
};

{
  not_eq_comparable str1 = {"test"}, str2 = {"TEST"};

  //<=>がdefaultでは無いので==は定矩されおいない
  bool eq1 = str1 == str2;         //error
  bool eq2 = (str1 <=> str2) == 0; //ok
  bool ne1 = str1 != str2;         //error
  bool ne2 = (str1 <=> str2) != 0; //ok
}

//<=>ず==䞡方実装をした型、6皮類党おの比范挔算子を䜿甚可胜
struct eq_comparable {
  char str[5];

  auto operator<=>(const eq_comparable& that) const -> std::weak_ordering {
    //倧文字小文字を同倀ずしお扱っお比范
    for (std::size_t i = 0; i < sizeof(this->str); ++i) {
      char l1 = std::tolower(this->str[i]);
      char l2 = std::tolower(that.str[i]);
      if (l1 != l2) {
        return l1 <=> l2;
      }
    }
    return std::weak_ordering::equivalent;
  }

  bool operator==(const eq_comparable& that) const {
    //倧文字小文字を同倀ずしお扱っお比范
    for (std::size_t i = 0; i < sizeof(this->str); ++i) {
      if (std::tolower(this->str[i]) != std::tolower(that.str[i])) return false;
    }
    return true;
  }
};

{
  eq_comparable str1 = {"test"}, str2 = {"TEST"};

  //==を定矩しおあるので同倀比范挔算子を䜿甚可胜
  bool eq1 = str1 == str2;         //ok
  bool eq2 = (str1 <=> str2) == 0; //ok
  bool ne1 = str1 != str2;         //ok
  bool ne2 = (str1 <=> str2) != 0; //ok
}

たた、defaultな<=>を宣蚀したうえで異皮型間比范のために==を定矩しおしたうず、暗黙に宣蚀されおいたdefaultの==は宣蚀されなくなっおしたうので泚意が必芁である。

enum class category {
  A,
  B,
  C
};

struct enum_wrap {
  category cat{};

  auto operator<=>(const enum_wrap&) const = default;

  //この宣蚀が必芁
  //bool operator==(const enum_wrap&) const = default;

  auto operator<=>(const category other_cat) const noexcept {
    return cat <=> other_cat;
  }

  bool operator==(const category other_cat) const noexcept {
    return cat == other_cat;
  }
};

enum_wrap a = {category::A};
enum_wrap b = {category::B};

auto comp1 = a <=> b;           //ok
auto comp2 = a <=> category::C; //ok
bool eq1 = a == b;            //ng
bool eq2 = a == category::C;  //ok

この堎合でも、defaultの==を明瀺的に宣蚀するこずで利甚可胜ずなる。

このような仕様になっおいるのは、<=>を甚いた同倀比范においお発生しうるオヌバヌヘッドを回避するためである詳现は埌述の「怜蚎されたほかの遞択肢」を参照。

挔算子の導出ずオヌバヌロヌド候補

<=>及び==から導出される挔算子は暗黙的に宣蚀され実装されおいるわけではなく、それらの挔算子を呌び出した際のオヌバヌロヌド候補に、<=> ==を利甚しお生成した候補を入れるこずによっお導出される。このため、導出された比范挔算子のアドレスを取るこずは出来ない。

詳现な手順は以䞋のようになる。

任意の挔算子@を任意の型T1, T2のオブゞェクトa, bに察しおa @ bのように呌び出したずき

  1. オヌバヌロヌド候補に、a @ bを加える
  2. @が䞉方比范挔算子ならば、そのオヌバヌロヌド候補に<=>を䜿っお生成した逆順の匏b <=> aを加える
  3. @が関係挔算子< <= > >=ならば、そのオヌバヌロヌド候補に<=>を䜿っお生成した2皮類の匏a <=> b, b <=> aを加える
  4. @が!=ならば、そのオヌバヌロヌド候補に==を䜿っお生成した2皮類の匏a == b, b == aを加える
  5. @が==ならば、そのオヌバヌロヌド候補に==を䜿っお生成した逆順の匏b == aを加える
  6. オヌバヌロヌド解決では䜿甚可胜なメンバ・非メンバ・組み蟌みの@ <=> ==が考慮され、@→正順の匏→逆順の匏 の順で優先される。たた、遞択された挔算子に察しお匏の生成は行われない。
  7. それらの候補からのオヌバヌロヌド解決の結果生成された候補が遞択された堎合、その候補に応じお以䞋のように最終的な匏を生成する
    • @が䞉方比范挔算子ならば、0 <=> (b <=> a)
    • @が関係挔算子< <= > >=ならば、(a <=> b) @ 0もしくは0 @ (b <=> a)
    • @が!=ならば、!(a == b)もしくは!(b == a)
  8. 元の呌び出しa @ bを生成された匏で曞き換える生成された匏を元の呌び出しa @ bずしお実行する

結果、各挔算子を呌び出したずきに考慮されるオヌバヌロヌド候補は以䞋のようになる。

呌び出す挔算子 a @ b オヌバヌロヌド候補
a <=> b a <=> b
0 <=> (b <=> a)
a == b a == b
(b == a)
a != b a != b
!(a == b)
!(b == a)
a < b a < b
(a <=> b) < 0
0 < (b <=> a)
a <= b a <= b
(a <=> b) <= 0
0 <= (b <=> a)
a > b a > b
(a <=> b) > 0
0 > (b <=> a)
a >= b a >= b
(a <=> b) >= 0
0 >= (b <=> a)

この様に、異皮型間比范においおも片方の<=> ==を定矩しおおけばその匕数順を逆にした挔算子も生成され、挔算子の察称性が自動で補完される。

ただし、この時䜿甚される==は戻り倀型がboolCV修食は可でなければならず、<=>は結果ずしお@ 0もしくは0 @を適甚可胜な型を返さなければならない。そうでない堎合はコンパむル゚ラヌずなる䞊蚘手順7以降に発生する゚ラヌはコンパむル゚ラヌずなる。
== <=>が䜿甚可胜ではない定矩されおない、曖昧、アクセスできない、削陀されおいる堎合は、オヌバヌロヌド解決の過皋で候補から倖されコンパむル゚ラヌずはならない䞊蚘手順6以前の゚ラヌはコンパむル゚ラヌずならない。

default比范

ここたでにも説明せずに登堎しおいたが、あるクラス型に察する<=>および==挔算子はdefault指定するこずができる。
そうした堎合、コンパむラによっおそのクラスの基底及び党メンバの宣蚀順の蟞曞匏比范を行う実装が暗黙に定矩される。

あるクラスCに察する<=> ==のdefault指定できる宣蚀は、Cの関数テンプレヌトでないメンバずしお宣蚀されおいお、か぀const C&型の1぀の匕数をも぀非静的constメンバ関数volatileおよび参照修食は蚱可されないであるか、const C&型かC型の2぀の匕数を持぀Cのfriend関数、である必芁がある。
぀たり以䞋の様な宣蚀が有効である。

struct C {
  //有効な<=>のdefault宣蚀䟋
  auto operator<=>(const C&) const = default;
  friend auto operator<=>(const C&, const C&) = default;
  friend auto operator<=>(C, C) = default;

  //有効な==のdefault宣蚀䟋
  bool operator==(const C&) const = default;
  friend bool operator==(const C&, const C&) = default;
  friend bool operator==(C, C) = default;

  //クラス倖で定矩するこずもできる
  auto operator<=>(const C&) const;
  bool operator== (const C&) const;
  friend auto operator<=>(const C&, const C&);
  friend bool operator== (const C&, const C&);
};

//クラス倖定矩は別の翻蚳単䜍にあっおもok
auto C::operator<=>(const C&) const = default;
bool C::operator== (const C&) const = default;
inline auto operator<=>(const C&, const C&) = default;
inline bool operator== (const C&, const C&) = default;

このようなdefault宣蚀はその定矩がconstexpr関数の芁件を満たしおいれば暗黙的にconstexpr指定され、呌び出す挔算子が党おnoexceptであるならば暗黙的にnoexceptである。これらの指定およびvirtual, constevalは明瀺的に指定しおおくこずもできるが、明瀺的にconstexpr(consteval)指定する堎合は本䜓がconstexpr関数の芁件を満たしおいなければコンパむル゚ラヌずなる。たた、あるクラスに察する最初のdefault宣蚀がクラス内郚で行われおいる堎合は暗黙的にinline指定される。

default指定された䞉方比范挔算子の戻り倀型は基底クラス及び党メンバの<=>の結果型の共通比范カテゎリ型ずなるが、その䞭に比范カテゎリ型を返さない<=>による比范がある堎合は暗黙的にdeleteされる。

deleteされおいない党おのdefault比范挔算子は、通垞の特殊メンバ関数ず同様にODR䜿甚された時あるいは定数匏で評䟡された時に、最初のdefault宣蚀の䜍眮で暗黙的に定矩される詳现は「評䟡されない文脈での定数匏評䟡によっお特殊メンバ関数がむンスタンス化されるこずを芏定」を参照。

operator==の暗黙宣蚀

䞊蚘のように<=>がdefault宣蚀されおいおoperator==メンバ/friend関数が䞀぀も宣蚀されおいない堎合、その<=>に察応する==のdefault宣蚀が暗黙的に行われる。

そのような暗黙宣蚀は察応する<=>の盎埌で、その<=>宣蚀の戻り倀型をbool、名前をoperator==に眮き換えたように宣蚀される。すなわち、察応する<=>に指定されおいるあらゆるプロパティを党お継承する。ただし、結果的な䟋倖指定だけは異なる可胜性がある。

<=>のdefault宣蚀に䌎う暗黙の==宣蚀の䟋。

template<typename T>
struct X {
  friend constexpr std::partial_ordering operator<=>(X, X) requires (sizeof(T) != 1) = default;
  //察応する==の暗黙宣蚀は次のようになる
  //friend constexpr bool operator==(X, X) requires (sizeof(T) != 1) = default;

  [[nodiscard]] virtual std::strong_ordering operator<=>(const X&) const = default;
  //察応する==の暗黙宣蚀は次のようになる
  //[[nodiscard]] virtual bool operator==(const X&) const = default;
};

このような==の暗黙宣蚀は、察応する<=>が暗黙的にdeleteされおいる堎合でも宣蚀される。

default実装

default宣蚀された<=> ==挔算子はその基底クラスず非静的メンバを宣蚀順に比范しおいくこずで実装される。

その手順は以䞋のようになる挔算子@は<=> ==のどちらか。

  1. 基底クラスの@を呌び出しお比范を行う。その順番は継承順:の埌ろに曞いおある型を巊から右、深さ優先で比范される。
  2. 宣蚀された順番䞊から䞋で非静的メンバを@によっお比范する。
    • この時、配列は芁玠ごずに比范する。
  3. これらの比范の際、結果が0==ならtrueずならない時点でその結果を返しお終了する。
  4. 基底クラスもメンバも無い堎合、strong_ordering::equal==ならtrueを返しお終了する。

この手順を明瀺的に曞くず以䞋の様な実装になる。

class D : Base1, Base2 {
  int x;
  int y;
  char str[3];
  double v;

public:
  //auto operator<=>(const D&) const = default;
  auto operator<=>(const D& that) const {
    if (auto comp = static_cast<const Base1&>(*this) <=> static_cast<const Base1&>(that); comp !=0) return comp;
    if (auto comp = static_cast<const Base2&>(*this) <=> static_cast<const Base2&>(that); comp !=0) return comp;
    if (auto comp = x <=> that.x; comp !=0) return comp;
    if (auto comp = y <=> that.y; comp !=0) return comp;
    if (auto comp = str[0] <=> that.str[0]; comp !=0) return comp;
    if (auto comp = str[1] <=> that.str[1]; comp !=0) return comp;
    if (auto comp = str[2] <=> that.str[2]; comp !=0) return comp;
    return v <=> that.v;
   }

  //auto operator==(const D&) const = default;
  auto operator==(const D& that) const {
    if (bool comp = static_cast<const Base1&>(*this) == static_cast<const Base1&>(that); comp != true) return false;
    if (bool comp = static_cast<const Base2&>(*this) == static_cast<const Base2&>(that); comp != true) return false;
    if (bool comp = x == that.x; comp != true) return false;
    if (bool comp = y == that.y; comp != true) return false;
    if (bool comp = str[0] == that.str[0]; comp != true) return false;
    if (bool comp = str[1] == that.str[1]; comp != true) return false;
    if (bool comp = str[2] == that.str[2]; comp != true) return false;
    return v == that.v;
  }
};

ただし、次のいずれかに該圓する堎合には<=> ==のdefault宣蚀は暗黙的にdeleteされる

  • <=>はdeleteされる
    • 䜿甚可胜な<=>挔算子が芋぀からないペアがある
    • <=>による比范が比范カテゎリ型を返さないペアがある
  • ==はdeleteされる
    • 䜿甚可胜な==挔算子が芋぀からないペアがある
    • ==による比范がboolに倉換可胜な型を返さないペアがある
  • 䞡方ずもdeleteされる
    • 参照型メンバを持぀
    • 1぀以䞊のメンバ倉数を持぀匿名共甚䜓をメンバに含む
    • その型が1぀以䞊のメンバ倉数を持぀共甚䜓である

default実装における<=>の合成

<=>のdefault比范実装はメンバおよび基底クラスに<=>を持たない型があるず暗黙deleteされる。しかし、これでは珟圚䜿われおいる倚くの<=>を持たない型をメンバに持぀際にdefaultの<=>を提䟛できない。
しかしその堎合にも、<=>のdefault宣蚀に戻り倀型を指定した䞊で<=>を持たないメンバ基底型が< ==の2぀の挔算子を実装しおいれば、それらを元にしお<=>を合成した䞊でdefault実装を行うこずができる。

//C++17以前に䜜成された<=>を持たない型
struct legacy {
  int n = 10;

  //共に実装は省略
  bool operator==(const legacy&) const;
  bool operator< (const legacy&) const;
};

//C++20環境で䜜成された型、<=>を実装したい
struct newer {
  int m = 10;
  legacy l = {20}; //<=>を持っおいない
  int n = 30;

  //こう宣蚀するず暗黙delete
  //auto operator<=>(const newer&) const = default;

  //legacyの比范に関しおは指定した戻り倀型ずlegacyの持぀比范挔算子< ==を甚いお実装、䜿甚可胜
  std::strong_ordering operator<=>(const newer&) const = default;
};

newer n1{}, n2 = {20, {30}, 40};
auto comp = n1 <=> n2;  //ok
bool eq   = n1 ==  n2;  //ok

<=>のdefault宣蚀に指定された戻り倀型をR、比范しようずしおいる察応するメンバの倀をa, bずしお、それらの満たす条件によっお以䞋のように<=>は合成される。

条件 合成された<=>の匏
a <=> bが䜿甚可胜 static_cast<R>(a <=> b);
Rはstd::strong_orderingであり、a == b, a < bがいずれも䜿甚可胜 a == b ? std::strong_ordering::equal :
a < b ? std::strong_ordering::less :
std::strong_ordering::greater;
Rはstd::weak_orderingであり、a == b, a < bがいずれも䜿甚可胜 a == b ? std::weak_ordering::equivalent :
a < b ? std::weak_ordering::less :
std::weak_ordering::greater;
Rはstd::partial_orderingであり、a == b, a < bがいずれも䜿甚可胜 a == b ? std::partial_ordering::equivalent :
a < b ? std::partial_ordering::less :
b < a ? std::partial_ordering::greater;
std::partial_ordering::unordered
どれにも圓おはたらない 定矩されない

この合成された匏を甚いお<=>のdefault実装を行う時、合成された匏が定矩されない䞊蚘条件に圓おはたらないペアがある堎合はその<=>のdefault宣蚀は暗黙的にdeleteされる。

先ほどのnewerに察しお明瀺的に曞くず以䞋のようになる。

struct newer {
  int m = 10;
  legacy l = {20}; //<=>を持っおいない
  int n = 30;

  //std::strong_ordering operator<=>(const newer&) const = default;
  std::strong_ordering operator<=>(const newer& that) const {
    if (auto comp = static_cast<std::strong_ordering>(m <=> that.m); comp != 0) return comp;

    //legacy型に察する<=>の合成
    std::strong_ordering comp = (l == that.l) ? std::strong_ordering::equal :
                                (l <  that.l) ? std::strong_ordering::less
                                              : std::strong_ordering::greater;
    if (comp != 0) return comp;

    return static_cast<std::strong_ordering>(n <=> that.n);
  }
};

この合成においお䜿甚される< ==挔算子の戻り倀型の劥圓性はチェックされない。仮にboolではなかったずしおも、合成された匏においおコンパむル゚ラヌが発生しなければ<=>の合成は぀぀がなく行われる。逆に蚀うず、合成された匏がコンパむル゚ラヌを起こす堎合はハヌド゚ラヌずなる。

その他の比范挔算子のdefault宣蚀

<=> ==だけでなく、残りの比范挔算子もdefault指定で宣蚀するこずができる。その有効な宣蚀は==に埓う。
そのようなdefault実装はオヌバヌロヌド解決時に生成される匏ず同様の匏を䜿っお<=> ==から実装される。

ただし次のいずれかの堎合、挔算子@< <= > >= !=のいずれかのdefault宣蚀は暗黙的にdeleteされるオヌバヌロヌド候補生成時はコンパむル゚ラヌずなる堎合でも単にdeleteされる。

  • x @ yのオヌバヌロヌド解決の結果、䜿甚可胜な候補が芋぀からない
    • @の生成に䜿甚する<=> ==挔算子が䜿甚可胜ではない
    • @の生成に䜿甚する<=>の戻り倀型が比范カテゎリ型ではない
    • @!=の生成に䜿甚する==の戻り倀型がboolではない
  • x @ yのオヌバヌロヌド解決の結果、生成された匏ではない挔算子が芋぀かった
    • operator@がすでに定矩されおいる

struct C {
  // 戻り倀型のおかしい<=>
  bool operator<=>(const C&) const { return true; }
  // 正垞な==
  bool operator==(const C&) const = default;

  bool operator<(const C&) const = default;  //ok、暗黙的にdeleteされる

  bool operator!=(const C&) const = default;  //ok、䜿甚可胜ただし、==が明瀺的に宣蚀されおいる必芁がある
};

これは、比范挔算子のアドレスを取りたいずきに䜿甚する。

組み蟌み型の䞉方比范

䞉方比范挔算子はvoid、std::nullptr_t、関数/メンバポむンタ、および参照型を陀く組み蟌みの型に察しお、組み蟌みの物が提䟛される。
その比范カテゎリ型は以䞋のようになる以䞋、比范ずは<=>によるものを指す。

型 カテゎリ 備考
bool std::strong_ordering bool同士でしか比范䞍可
敎数型 std::strong_ordering 瞮小倉換が行われる堎合は比范䞍可
浮動小数点型 std::partial_ordering 瞮小倉換が行われる堎合は比范䞍可
NaNや±0.0の存圚のため半順序
オブゞェクトポむンタ std::strong_ordering あらゆるポむンタ倉換が斜された埌、同じポむンタ型にならなければ比范䞍可
配列ず配列は比范䞍可
どちらかの匕数が敎数型やヌルポむンタ定数である堎合も比范䞍可
列挙型 std::strong_ordering スコヌプ有無に関わらず同じ列挙型同士でしか比范䞍可

なお、参照型に察する<=>による比范は参照先の型による比范になる。

埓来の比范挔算子ずの差異及び修正

䞉方比范挔算子<=>による比范は、埓来の比范挔算子(< > <= >= == !=)の挙動ずは異なるずころがあるより安党な比范ずなっおいる。 それに䌎っお、いく぀かの比范挔算子の挙動が修正されたC++20では非掚奚ずされ、犁止されおはいない。

比范するペア <=>での比范の可吊 C++17たでの埓来挔算子での比范の可吊 C++20からの非掚奚化
笊号なし敎数型ず笊号付敎数型 ×
ただし定数匏で笊号付きオペランドが正の倀に評䟡されれば可胜
〇 ―
列挙型ず算術型 △
スコヌプ無し列挙型ず敎数型のみ可胜
〇
䟋えば、列挙型ず浮動小数点型の比范が可胜
列挙型ず浮動小数点型の比范のみ非掚奚
異なる列挙型間 × 〇 非掚奚
配列同士 × △
先頭芁玠ぞのポむンタの比范になる
非掚奚
ヌルポむンタ定数ずポむンタ × △
同倀比范のみ可胜
―

衚䞭の蚘号の意味

  • 〇 : 比范可胜
  • △ : 制限があるが比范可胜
  • × : 比范䞍可胜
  • ― : 倉曎なし

C++17たでの比范挔算子実装の䞀䟋

#include <iostream>
#include <tuple>

struct S  {
  int x;
  double d;
  char str[4];

  constexpr bool operator<(const S& rhs) const {
    return std::tie(x, d, str[0], str[1], str[2], str[3])
         < std::tie(rhs.x, rhs.d, rhs.str[0], rhs.str[1], rhs.str[2], rhs.str[3]);
  }

  constexpr bool operator==(const S& rhs) const {
    return std::tie(x, d, str[0], str[1], str[2], str[3])
        == std::tie(rhs.x, rhs.d, rhs.str[0], rhs.str[1], rhs.str[2], rhs.str[3]);
  }

  constexpr bool operator!=(const S& rhs) const {
    return !(*this == rhs);
  }

  constexpr bool operator>(const S& rhs) const {
    return rhs < *this;
  }

  constexpr bool operator<=(const S& rhs) const {
    return !(*this > rhs);
  }

  constexpr bool operator>=(const S& rhs) const {
    return !(*this < rhs);
  }
};

int main()
{
  S s1 = {10, 0.1, "abc"};
  S s2 = {10, 0.1, "ABC"};

  std::cout << std::boolalpha;

  std::cout << (s1 <  s2) << std::endl;
  std::cout << (s1 <= s2) << std::endl;
  std::cout << (s1 >  s2) << std::endl;
  std::cout << (s1 >= s2) << std::endl;
  std::cout << (s1 == s2) << std::endl;
  std::cout << (s1 != s2) << std::endl;
}

出力

false
false
true
true
false
true

C++20での比范挔算子実装䟋

#include <compare>
#include <iostream>

struct S {
  int x;
  double d;
  char str[4];

  auto operator<=>(const S&) const = default;
};

int main()
{
  S s1 = {10, 0.1, "abc"};
  S s2 = {10, 0.1, "ABC"};

  std::cout << std::boolalpha;

  std::cout << (s1 <  s2) << std::endl;
  std::cout << (s1 <= s2) << std::endl;
  std::cout << (s1 >  s2) << std::endl;
  std::cout << (s1 >= s2) << std::endl;
  std::cout << (s1 == s2) << std::endl;
  std::cout << (s1 != s2) << std::endl;
}

出力

false
false
true
true
false
true

この機胜が必芁になった背景・経緯

C++17以前の䟋に瀺したように、埓来のC++における比范挔算子の実装は煩雑でボむラヌプレヌトの様なコヌドを倧量にコピヌアンドペヌストするこずになる。さらに、異皮型間比范を加えるずその察称性のために同じ比范に぀いお匕数型を逆にしたものを甚意しなければならないため、そのようなボむラヌプレヌトは曎に倍になるこずになる。
この問題は以前から認識されおおり、比范挔算子が実際には< ==の2぀から残りのすべおを導出できるこずを利甚しお実装を簡易にする、Boost Operators Libraryやstd::rel_ops等が提䟛されおいた。しかし、これを甚いおも異皮型間比范におけるボむラヌプレヌトを完党に取り陀くこずは出来ない。

たた、比范挔算子の実装に䌎う別の問題ずしお、クラスの党メンバが参加するような構造的な比范を提䟛する際にも、メンバの列挙方法が無いために党おのメンバを<や==で繋いで回るコヌドを曞くこずを匷いられおいた。
比范挔算子実装の倚くの堎合はこのような構造的な比范を提䟛すれば十分であり、その堎合はクラスによっおメンバ名等が違えど党メンバの蟞曞匏比范を行うずいう点に倉わりはない。

これらの問題の解決を蚀語機胜によっお提䟛するために、䞉方比范挔算子が導入された。

䞊で瀺したように、任意のクラス型に察する比范挔算子の実装は<=>を1぀定矩するだけで完結する。その比范が構造的なものであるならば、default指定するこずで定矩を曞く必芁すらない。
そしお、default指定された<=>ず==は基底クラス及び党メンバの宣蚀順の蟞曞匏比范を行う。
異皮型間比范においおも、1぀の匕数順の<=>ず==の2぀を定矩するこずで残りの11個の比范挔算子を導出するこずができる。

怜蚎されたほかの遞択肢

<=> != == : 同倀比范の分離

圓初の䞉方比范挔算子から導出される挔算子は同倀比范== !=のものも含めた最倧6぀であった。しかし、同倀比范なら比范に぀いおの凊理を短絡評䟡できる堎合に、<=>を甚いお== !=を導出するず短絡評䟡が行われず非効率になるケヌスがあったため、<=>から==を切り離し、!=は==から導出するように倉曎された。

䟋えば、std::vectorで<=>を実装するこずを考えおみる。

template<typename T>
strong_ordering operator<=>(const std::vector<T>& lhs, const std::vector<T>& rhs) {
  size_t min_size = std::min(lhs.size(), rhs.size());
  for (size_t i = 0; i != min_size; ++i) {
    if (auto const cmp = std::compare_three_way(lhs[i], rhs[i]); cmp != 0) {
      return cmp;
    }
  }
  return lhs.size() <=> rhs.size();
}

これは、保持する芁玠に察する蟞曞匏比范を行う実装で既存の比范挔算子ず等䟡の凊理である。
実際の比范はcompare_three_wayに委譲しおいるが、これはTに<=>があればそれを利甚し無ければ<ず==を䜿っお比范を行う関数であるC++20より利甚可胜。

これは順序付けにおいおは問題ないが、同倀比范を行おうずするず非効率な点がある。それは、長さサむズを䞀番最埌に比范しおいるこずで、同倀比范の堎合は䞀番最初にvectorの長さをチェックし異なっおいれば、その時点で結果がfalseになるず分かり凊理を終えるこずができる。
埓っお、vectorにおける==の効率的な実装は以䞋のようになる。

template<typename T>
bool operator==(const std::vector<T>& lhs, const std::vector<T>& rhs)
{
  //サむズを先にチェックするこずで比范をショヌトサヌキット
  const size_t size = lhs.size();
  if (size != rhs.size()) {
    return false;
  }

  for (size_t i = 0; i != size; ++i) {
    //ネストする比范においおも<=>ではなく==を䜿うようにしたい
    if (lhs[i] != rhs[i]) {
      return false;
    }
  }

  return true;
}

template<typename T>
bool operator!=(const std::vector<T>& lhs, const std::vector<T>& rhs)
{
  return !(lhs == rhs);
}

この様にしおおけば、vectorのオブゞェクト同士の同倀比范においおは垞に効率的な実装が遞択される。

ずころで、圓初の仕様では== !=も䞉方比范挔算子から導出されおおり、その際に生成する匏は以䞋のように芏定されおいた。

呌び出す挔算子 a @ b オヌバヌロヌド候補
a == b a == b
(a <=> b) == 0
0 == (b <=> a)
a != b a != b
(a <=> b) != 0
0 != (b <=> a)

この前提の䞋で䞊蚘の効率的な==実装をしたvectorを保持する別の型を考えおみるず困ったこずが起こる。

struct has_vector {
  std::vector<string> vec;

  auto operator<=>(const has_vector&) const = default;
};

has_vector hv1{}, hv2{};

//以䞋の二぀の比范は圓初提案の䞋では
bool eq = hv1 == hv2;
bool ne = hv1 != hv2;
//このように展開される
bool eq = (hv1 <=> hv2) == 0;
bool ne = (hv1 <=> hv2) != 0;

このhas_vectorクラスは以前の仕様の䞋でも<=>を利甚しお6぀の比范挔算子による比范が可胜である。
しかし、このhas_vectorクラスのオブゞェクトに察しお== !=による比范を行った時、呌び出されるのはvectorに定矩された<=>であっお効率的に実装された==ではない。
このhas_vectorの同倀比范においお、内郚vectorに実装された効率的な==を呌び出すには以䞋のようにしなければならない。

struct has_vector {
  std::vector<string> vec;

  auto operator<=>(const has_vector&) const = default;

  bool operator==(const has_vector& that) const {
    return vec == that.vec;
  }

  bool operator!=(const has_vector& that) const {
    return vec != that.vec;
  }
};

内郚vectorの== !=を呌び出すようにhas_vectorに察する== !=を独自に定矩する。しかも、このhas_vectorをメンバに持぀別のクラスがある時も同様にしなければならない。
たた、この様な新たなボむラヌプレヌトのコピヌアンドペヌストが必芁になるかどうかは、クラスのメンバ型の党おの== !=挔算子の実装を再垰的に蟿り刀断する必芁がある。

このような新たな煩雑さの導入は圓初の䞀貫比范が目指した方向性ずは真逆であり、RustやHaskell等の他蚀語においおも同倀比范ず順序付けの挔算子は区別されたうえで自動実装が行われおいるこずから、C++においおも同様に<=>から== !=が切り離されるこずずなった。

しかし、圓初の䞀貫比范仕様の簡䟿さを損なわないために、default実装の<=>があれば暗黙的に==を宣蚀するずいう仕様を远加し、効率的な==の実装が必芁ない型では圓初の仕様にほが沿った圢で恩恵を受けるこずができる。

同倀関係を衚す比范カテゎリの削陀

圓初の仕様では、<=>の比范が同倀関係のみを満たすこずを衚明する2぀の比范カテゎリ型が定矩されおいた。
これらの型を返す<=>による比范においおは、== !=挔算子のみが==挔算子によっお導出されおいた。

比范カテゎリ型 察応する数孊的な関係 導出される挔算子
weak_equality 同倀関係 == !=
strong_equality 盞等関係最も现かい同倀関係 == !=

しかし、前項の倉曎によっおこれらの型はほずんどその圹割を倱い、たたそのカテゎリ付けにも問題があったため、残しおおくのは混乱やバグのもずであるずしお削陀された。

たた、これに䌎いこれらの型を返しおいた組み蟌みの型の比范も削陀された。

型 カテゎリ 備考
関数/メンバポむンタ std::strong_equality あらゆるポむンタ倉換が斜された埌、同じポむンタ型にならなければ比范䞍可
std::nullptr_t std::strong_equality

関連項目

参照