默认比较 (C++20 起)
可以通过将比较运算符函数显式预置的方式要求编译器为某个类生成对应的默认比较。
定义
预置比较运算符函数 是满足以下所有条件的非模板比较运算符函数(即 <=>
, ==
, !=
, <
, >
, <=
, or >=
):
- 它是某个类
C
的非静态成员或友元。 - 它是在
C
中或在C
是完整类型的语境中定义为预置的。 - 它有两个类型是 const C& 的形参或两个类型是
C
的形参,其中隐式对象形参(如果存在)会被视为第一个形参。
这样的比较运算符函数也被称为关于类 C
的预置比较运算符函数。
struct X { bool operator==(const X&) const = default; // OK bool operator==(const X&) = default; // 错误:隐式对象形参类型是 X& bool operator==(this X, X) = default; // OK }; struct Y { friend bool operator==(Y, Y) = default; // OK friend bool operator==(Y, const Y&) = default; // 错误:形参类型不同 }; bool operator==(const Y&, const Y&) = default; // 错误:不是 Y 的友元
比较运算符函数的隐式定义中的名字查找和访问检查会在等价于该函数的函数体的语境中进行。在类中出现的将比较运算符函数预置的定义必须是该函数的首个声明。
默认比较顺序
给定类 C
,按以下顺序组成子对象列表:
-
C
的直接基类子对象,按声明顺序。 -
C
的非静态数据成员,按声明顺序。
- 如果有数组类型的成员,那么它们会被展开成它们的元素序列,按下标升序。展开是递归的:类型也是数组的数组元素会被继续展开,直到不存在数组类型的子对象。
对于类型 C
的任意对象 x,在后续描述中:
- 设 n 为关于 x 的(扩展后的)子对象列表。
- 设 x_i 为关于 x 的(扩展后的)子对象列表中的第 i 个子对象,其中 x_i 通过将派生类到基类转换、类成员访问表达式和数组下标表达式应用到 x 的序列组成。
struct S {}; struct T : S { int arr[2][2]; } t; // “t” 的子对象列表按顺序包含以下 5 个子对象 // (S)t → t[0][0] → t[0][1] → t[1][0] → t[1][1]
三路比较
关于类类型的 operator<=> 可以以任意返回类型定义为预置。
比较类别类型
有以下三种比较类别类型:
类型 | 等价的值 | 无法比较的值 |
---|---|---|
std::strong_ordering | 不可以被区分 | 不允许比较 |
std::weak_ordering | 可以被区分 | 不允许比较 |
std::partial_ordering | 可以被区分 | 允许比较 |
合成三路比较
对于具有相同类型的泛左值 a 与 b 之间的 T
类型合成三路比较 定义如下:
- 如果对 a <=> b 的重载决议产生了可用候选,并且可以通过
static_cast
显式转换到T
,那么合成比较是 static_cast<T>(a <=> b)。 - 否则,如果满足以下任意条件,那么合成比较未定义:
- 对 a <=> b 的重载决议找到了至少一个可行候选。
-
T
不是比较类别类型。 - 对 a == b 的重载决议没有产生可用候选。
- 对 a < b 的重载决议没有产生可用候选。
- 否则,如果
T
是 std::strong_ordering,那么合成比较是:
a == b ? std::strong_ordering::equal : a < b ? std::strong_ordering::less : std::strong_ordering::greater
- 否则,如果
T
是 std::weak_ordering,那么合成比较是:
a == b ? std::weak_ordering::equivalent : a < b ? std::weak_ordering::less : std::weak_ordering::greater
- 否则,如果
T
是 std::partial_ordering),那么合成比较是:
a == b ? std::partial_ordering::equivalent : a < b ? std::partial_ordering::less : b < a ? std::partial_ordering::greater : std::partial_ordering::unordered
占位返回类型
如果预置的关于类类型 C
的三路比较运算符函数(operator<=>)声明的返回类型是 auto,那么返回类型会从 C
类型对象 x 的对应子对象的三路比较的返回类型推导。
对于关于 x 的(扩展后的)子对象列表中的每个子对象 x_i:
- 对 x_i <=> x_i 进行重载决议,如果重载决议没有产生可用候选,那么预置的 operator<=> 会被定义为弃置。
- 以
R_i
表示 x_i <=> x_i 的类型的无 cv 限定版本,如果R_i
不是比较类别类型,那么预置的 operator<=> 会被定义为弃置。
如果预置的 operator<=> 没有被定义为弃置,那么它的返回类型会被推导为 std::common_comparison_category_t<R_1, R_2, ..., R_n>。
非占位返回类型
如果预置的三路比较运算符函数(operator<=>)声明的返回类型不是 auto,那么它就不能包含任何占位类型(例如 decltype(auto))。
如果关于 x 的(扩展后的)子对象列表中存在子对象 x_i 使得 x_i 与 x_i 之间的声明返回类型的合成三路比较未定义,那么预置的 operator<=> 会被定义为弃置。
比较结果
设 x 和 y 为预置的 operator<=> 的形参,将 x 和 y 的(扩展后的)子对象列表的每个子对象分别记为 x_i 和 y_i。x 与 y 的默认三路比较会通过以 i 的升序依次比较对应的子对象 x_i 与 y_i。
设 R
为(可能经过推导的)返回类型,x_i 与 y_i 的比较结果是 x_i 与 x_i 之间的 R
类型的三路比较结果。
- 在进行 x 与 y 的默认三路比较过程中,如果子对象 x_i 与 y_i 之间的比较产生了结果 v_i 使得将 v_i != 0 按语境转换到 bool 会产生 true,那么返回值是 v_i 的副本(不会比较其余子对象)。
- 否则,返回值是 static_cast<R>(std::strong_ordering::equal)。
#include <compare> #include <iostream> #include <set> struct Point { int x; int y; auto operator<=>(const Point&) const = default; /* 非比较函数 */ }; int main() { Point pt1{1, 1}, pt2{1, 2}; std::set<Point> s; // OK s.insert(pt1); // OK // 不需要显式定义双路比较运算符函数: // operator== 会隐式声明(见下文) // 而其他运算符的重载决议会选择重写候选 std::cout << std::boolalpha << (pt1 == pt2) << ' ' // false << (pt1 != pt2) << ' ' // true << (pt1 < pt2) << ' ' // true << (pt1 <= pt2) << ' ' // true << (pt1 > pt2) << ' ' // false << (pt1 >= pt2) << ' '; // false }
相等比较
显式声明
关于类类型的 operator== 可以以 bool 返回类型定义为预置。
给定类 C
和 C
类型的对象 x,如果关于 x 的(扩展后的)子对象列表中存在子对象 x_i 使得对 x_i == x_i 的重载决议没有产生可用候选,那么预置的 operator== 会被定义为弃置。
设 x 和 y 为预置的 operator== 的形参,将 x 和 y 的(扩展后的)子对象列表的每个子对象分别记为 x_i 和 y_i。x 与 y 的默认相等比较会通过以 i 的升序依次比较对应的子对象 x_i 与 y_i。
x_i 与 y_i 的比较结果是 x_i == y_i 的结果。
- 在进行 x 与 y 的默认相等比较过程中,如果子对象 x_i 与 y_i 之间的比较产生了结果 v_i 使得将 v_i 按语境转换到 bool 会产生 false,那么返回值是 false(不会比较其余子对象)。
- 否则,返回值是 true。
#include <iostream> struct Point { int x; int y; bool operator==(const Point&) const = default; /* 非比较函数 */ }; 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== 未定义 }
隐式声明
如果类 C
没有显式声明任何名为 operator== 的成员或友元,那么对于每个定义为预置的 operator<=> 都会隐式声明一个 运算符。每个隐式声明的 operator== 都会与对应的预置 operator<=> 具有相同的访问和函数定义,并且在相同的类作用域中,但有以下不同:
- 声明符标识符会被替换成 operator==。
- 返回类型会被替换成 bool。
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; };
次级比较
关于类类型的次级比较运算符函数(!=
、<
、>
、<=
和 >=
)可以以 bool 返回类型定义为预置。
设 @
为五个次级比较运算符之一,对于每个形参是 x 和 y 的预置 operator@ 都会进行最多两次重载决议(预置的 operator@ 不会被视为候选)以确定它是否会被定义为弃置。
- 第一次重载决议对 x @ y 进行。如果重载决议没有产生可用候选,或者选择的候选不是重写候选,那么预置的 operator@ 会被定义为弃置。这些情况下不会进行第二次重载决议。
- 第二次重载决议对 x @ y 的重写候选进行。如果重载决议没有产生可用候选,那么预置的 operator@ 会被定义为弃置。
如果 x @ y 不能隐式转换到 bool,那么预置的 operator@ 会被定义为弃置。
如果预置的 operator@ 没有被定义为弃置,那么它会产生 x @ y.
struct HasNoRelational {}; struct C { friend HasNoRelational operator<=>(const C&, const C&); bool operator<(const C&) const = default; // OK,函数被预置 };
关键词
缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
缺陷报告 | 应用于 | 出版时的行为 | 正确行为 |
---|---|---|---|
CWG 2539 | C++20 | 合成三路比较即使在无法进行显式转换时也会选择 static_cast | 此时不会选择 static_cast |
CWG 2546 | C++20 | 对 x @ y 的重载决议选择了不可用的重写候选时 operator@ 不会被定义为弃置 | 此时会被定义为弃置 |
CWG 2547 | C++20 | 不明确是否可以预置对非类的比较运算符函数 | 不可以预置 |
CWG 2568 | C++20 | 比较运算符函数的隐式定义可能会违反成员访问规则 | 会在等价于它们的函数体 的语境中进行访问检查 |