Default comparisons (since C++20)
比较运算符函数可以显式默认化,以请求编译器为类生成相应的默认比较实现。
目录 |
定义
一个
默认比较运算符函数
是满足以下所有条件的非模板比较运算符函数(即
<=>
、
==
、
!=
、
<
、
>
、
<=
或
>=
):
-
它是某个类
C的 非静态成员 或 友元 。 -
它在
C中或C为 完整类型 的上下文中 被定义为默认函数 。 -
它有两个参数,类型为
const
C
&
或类型为
C,其中 隐式对象参数 (如果存在)被视为第一个参数。
此类比较运算符函数被称为
类
C
的默认比较运算符函数
。
struct X { bool operator==(const X&) const = default; // 正确 bool operator==(const X&) = default; // 错误:隐式对象参数类型为 X& // 与显式参数类型不匹配 bool operator==(this X, X) = default; // 正确 }; struct Y { friend bool operator==(Y, Y) = default; // 正确 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 | 可区分 | 允许存在 |
合成三路比较
类型
T
在相同类型的泛左值
a
与
b
之间的
合成三路比较
按以下方式定义:
-
如果对
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 <=> 被定义为删除。
-
将
x_i
<=>
x_i
类型的 cv 非限定版本记作
R_i,若R_i不是比较类别类型,则默认的 operator <=> 被定义为删除。
如果默认的 operator <=> 未被定义为删除,其返回类型被推导为 std:: common_comparison_category_t < R_1, R_2, ..., R_n > 。
非占位符返回类型
如果默认的 operator <=> 声明的返回类型不是 auto ,则它不能包含任何 占位符类型 (例如 decltype ( auto ) )。
如果在(展开后的)子对象列表中存在子对象 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
之间的比较结果,即为类型为
R
的
x_i
与
y_i
之间合成的三路比较结果。
- 在 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; // 正确 s.insert(pt1); // 正确 // 无需显式定义双向比较运算符函数: // 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
及其类型对象
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 经上下文转换到 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; // 正确:函数被默认实现 };
关键词
缺陷报告
下列行为变更缺陷报告被追溯应用于先前发布的 C++ 标准。
| 缺陷报告 | 应用于 | 发布时的行为 | 正确行为 |
|---|---|---|---|
| CWG 2539 | C++20 |
合成的三路比较会选择
static_cast ,即使显式转换不可用 |
在此情况下不选择
static_cast |
| CWG 2546 | C++20 |
默认的次要
operator@
在
x @ y 的重载解析选择了 不可用的重写候选时未被定义为删除 |
在此情况下
定义为删除 |
| CWG 2547 | C++20 |
未明确非类的比较运算符
函数是否可默认生成 |
不可默认生成 |
| CWG 2568 | C++20 |
比较运算符函数的隐式定义
可能违反成员访问规则 |
访问检查在其函数体
等效的上下文中执行 |