默认比较(C++20 起)

来自cppreference.com
< cpp‎ | language

提供一种方式,以要求编译器为某个类生成相一致的比较运算符。

语法

返回类型 类名::operator运算符( const 类名 & ) const &(可选) = default; (1)
friend 返回类型 operator运算符( const 类名 &, const 类名 & ) = default; (2)
friend 返回类型 operator运算符( 类名, 类名 ) = default; (3)
运算符 - 比较运算符(<=>==!=<><=,或 >=
返回类型 - 运算符函数的返回类型

解释

1) 将默认比较函数声明为成员函数。
2) 将默认比较函数声明为非成员函数。
3) 将默认比较函数声明为非成员函数。参数按值传递。

每当有值通过 <><=>=,或 <=> 被比较且重载决议选择该重载时,三路比较函数(不管是否为默认)会被调用。

每当有值通过 ==!= 被比较且重载决议选择该重载时,相等比较函数(不管是否为默认)会被调用。

与默认的特殊成员函数类似,默认的比较函数在被 ODR 使用被常量求值所需时被定义。

默认比较

默认三路比较

默认的 operator<=> 通过依次以计算 <=> 比较基类(从左到右,深度优先),然后是非静态成员(按声明顺序)子对象,递归地展开数组成员(按下标递增),并在发现不相等的结果时提前停止的方式执行字典序比较,即:

for /* T 的每个基类或子对象 o */
   if (auto cmp = static_cast<R>(compare(lhs.o, rhs.o)); cmp != 0) return cmp;
return static_cast<R>(strong_ordering::equal);

虚基子对象是否会多次被比较未被指明。

如果被声明的返回类型是 auto,实际的返回类型是要被比较的基类,成员子对象和成员数组元素的公共比较类别(见 std::common_comparison_category)。这样在编写返回类型非平凡地依赖于成员的场合时会更容易,例如:

template<class T1, class T2>
struct P {
 T1 x1;
 T2 x2;
 friend auto operator<=>(const P&, const P&) = default;
};

将返回类型设为 R ,每一对子对象 a, b 按如下方法进行比较:

  • 如果 a <=> b 可用,比较结果为 static_cast<R>(a <=> b)
  • 否则,如果 operator<=> 的重载决议为 a <=> b 执行且至少找到一个候选,比较未定义(operator<=> 被定义为弃置的)。
  • 否则,如果 R 不是一个比较类别(见下文)或 a == ba < b 有其一不可用,比较未定义(operator<=> 被定义为弃置的)。
  • 否则,如果 Rstd::strong_ordering,结果是
a == b ? R::equal :
a < b  ? R::less :
         R::greater
  • 否则,如果 Rstd::weak_ordering,结果是
a == b ? R::equivalent :
a < b  ? R::less :
         R::greater
  • 否则(Rstd::partial_ordering),结果是
a == b ? R::equal :
a < b  ? R::less :
b < a  ? R::greater : 
         R::unordered

与任何 operator<=> 重载的规则一样,默认的 <=> 重载也允许类型被 <><=,和 >= 比较。


如果 operator<=> 是默认版本且 operator== 完全没有被声明,那么 operator== 将隐式地采用默认版本。

#include <compare>
struct Point {
  int x;
  int y;
  auto operator<=>(const Point&) const = default;
  // ... 非比较函数 ...
};
// 编译器生成全部六个比较运算符
 
#include <iostream>
#include <set>
int main() {
  Point pt1{1, 1}, pt2{1, 2};
  std::set<Point> s; // ok
  s.insert(pt1);     // ok
  std::cout << std::boolalpha
    << (pt1 == pt2) << ' ' // false;operator== 隐式地采用默认版本
    << (pt1 != pt2) << ' ' // true
    << (pt1 <  pt2) << ' ' // true
    << (pt1 <= pt2) << ' ' // true
    << (pt1 >  pt2) << ' ' // false
    << (pt1 >= pt2) << ' ';// false
}

默认相等比较

类可以定义 operator== 为默认版本,它返回一个 bool 值。这会以声明顺序对每个基类和成员子对象生成一轮相等比较。两个对象在它们每个基类和成员相等时相等。该检测会以声明顺序在找到基类或成员里出现不相等的情况下短路。

与任何 operator== 重载的规则一样,不等测试也能被允许:

struct Point {
  int x;
  int y;
  bool operator==(const Point&) const = default;
  // ... 非比较函数 ...
};
// 编译器生成分成员的相等比较
 
#include <iostream>
int main() {
  Point pt1{3, 5}, pt2{2, 5};
  std::cout << std::boolalpha
    << (pt1 != pt2) << '\n'  // true
    << (pt1 == pt1) << '\n'; // true
  struct [[maybe_unused]] { int x{}, y{}; } p, q;
  // if (p == q) { } // 错误:'operator==' 未被定义
}

其他默认比较操作符

四个关系运算符(<><=>=)均可以显式指定为默认版本。默认的关系运算符必须返回 bool

如果 x <=> y 的重载决议(包括参数交换后的 operator<=>)失败,或 operator@ 无法被应用到 x <=> y 的结果,该操作符是弃置的。否则,默认的 operator@ 在重载决议选择参数顺序不变的 operator<=> 时调用 x <=> y @ 0 ,否则调用 0 @ y <=> x

struct HasNoRelational {};
 
struct C {
  friend HasNoRelational operator<=>(const C&, const C&);
  bool operator<(const C&) = default;                       // OK:函数被弃置
};

与此类似,operator!= 也可以显式指定为默认版本。如果 x <=> y 的重载决议失败,或 x == y 结果的类型不为 bool,它也会被弃置。默认的 operator!= 会根据重载决议的选择调用 !(x == y) 或者 !(y == x)

指定关系运算符使用默认版本可用于创建可取址的函数。其他场合仅需提供 operator<=>operator==

自定义的比较和比较类别

在默认语义不适用的情况下,例如成员不能按顺序比较,或者不能采用自然比较,那么程序员可以自定义 operator<=> 并让编译器生成合适的比较运算符。比较运算符的种类由用户定义的 operator<=> 决定。

返回类型有三种:

返回类型 运算符 等价的值 无法比较的值
std::strong_ordering == != < > <= >= 不可以被区分 不允许比较
std::weak_ordering == != < > <= >= 可以被区分 不允许比较
std::partial_ordering == != < > <= >= 可以被区分 允许比较

强序

在这个自定义 operator<=> 返回 std::strong_ordering 的例子中,该操作符比较了类的每个成员,只是顺序不同(在这里名在姓前面比较)。

#include <compare>
#include <string>
struct Base {
    std::string zip;
    auto operator<=>(const Base&) const = default;
};
struct TotallyOrdered : Base {
  std::string tax_id;
  std::string first_name;
  std::string last_name;
public:
 // 自定义 operator<=> ,因为我们希望(在比较姓前)先比较名:
 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;
 }
 // ... 非比较函数 ...
};
// 编译器生成全部四个关系运算符
 
#include <cassert>
#include <set>
int main() {
  TotallyOrdered to1{"a","b","c","d"}, to2{"a","b","d","c"};
  std::set<TotallyOrdered> s; // OK
  s.insert(to1); // OK
  assert(to2 <= to1); // OK,调用 <=> 一次
}

注:返回 std::strong_ordering 的操作符需要比较每个成员,因为如果有成员没有被比较,可替换性会受牵连:两个可被区分的值有可能会比较相等。

弱序

在这个自定义 operator<=> 返回 std::weak_ordering 的例子中,该操作符不分大小写地比较了类的字符串成员:这和默认比较不同(因此需要自定义运算符)且通过这种方式比较相等的两个字符串可能可以被区分。

class CaseInsensitiveString {
  std::string s;
public:
  std::weak_ordering operator<=>(const CaseInsensitiveString& b) const {
    return case_insensitive_compare(s.c_str(), b.s.c_str());
  }
  std::weak_ordering operator<=>(const char* b) const {
    return case_insensitive_compare(s.c_str(), b);
  }
  // ... 非比较函数 ...
};
 
// 编译器生成全部四个关系运算符
CaseInsensitiveString cis1, cis2;
std::set<CaseInsensitiveString> s; // OK
s.insert(/*...*/); // ok
if (cis1 <= cis2) { /*...*/ } // OK,执行一次比较操作
 
// 编译器也生成了全部八个混合参数的关系运算符
if (cis1 <= "xyzzy") { /*...*/ } // OK,执行一次比较操作
if ("xyzzy" >= cis1) { /*...*/ } // OK,语义相同

注:这个例子展示了参数类型不同的 operator<=> 的效果:它生成了双向的不同类型参数的比较。

偏序

偏序是一种允许无法比较(无序)的值的比较的排序,比如包括 NaN 值的浮点排序,或在这个例子里的没有关联的人:

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 per1, per2;
if (per1 < per2) { /*...*/ } // OK,per2 是 per1 的先人
else if (per1 > per2) { /*...*/ } // OK,per1 是 per2 的先人
else if (std::is_eq(per1 <=> per2)) { /*...*/ } // OK,per1 是 per2(同一人)
else { /*...*/ } // per1 与 per2 没有(直系)关联
if (per1 <= per2) { /*...*/ } // OK,per2 是 per1 或 per1 的先人
if (per1 >= per2) { /*...*/ } // OK,per1 是 per2 或 per2 的先人
if (std::is_neq(per1 <=> per2)) { /*...*/ } // OK,per1 不是 per2(但可能有直系关联)

参阅