默认比较(C++20 起)
提供一种方式,以要求编译器为某个类生成相一致的比较运算符。
语法
返回类型 类名::operator 运算符( const 类名 & ) const & (可选) = default;
|
(1) | ||||||||
friend 返回类型 operator 运算符( const 类名 & , const 类名 & ) = default;
|
(2) | ||||||||
friend 返回类型 operator 运算符( 类名, 类名 ) = default;
|
(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 == b 与 a < b 有其一不可用,比较未定义(
operator<=>
被定义为弃置的)。 - 否则,如果 R 是 std::strong_ordering,结果是
a == b ? R::equal : a < b ? R::less : R::greater
- 否则,如果 R 是 std::weak_ordering,结果是
a == b ? R::equivalent : a < b ? R::less : R::greater
- 否则(R 是 std::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(但可能有直系关联)