The Space-ship Operator <=>
Totality & Interchangeability
这两个点是区分 strong ordering, weak ordering 和 partial ordering 的关键
totality 是说,给定 a 和 b
- a < b
- a == b
- a > b
一定有一个成立
不具备 totality 的类型例如
- 集合
{1, 2}和{2, 3},二者即不相等,也没有其中一个是另一个的子集 - 浮点数中某个值是 NaN,直接无法比较
Interchangeable 则是说:如果 a == b 满足,则 a 和 b 可以互相替代,二者可以视作完全一样
- 满足的例子是 integer 里,a 和 b 都是 42
- 不满足的例子是 case-insensitive strings,例如
"hello"和"HELLO";不考虑大小写二者相等,但是二者不能互相替换
Ordering
基于上面的定义:
| Totality | Interchangeable | |
|---|---|---|
| Strong Ordering | Hold | Yes |
| Weak Ordering | Hold | No |
| Partial Ordering | May Not Hold for some values / UnOrdered |
另外需要注意:
- strong ordering 的 equivalent 和 equal 是等价的
- weak ordering 只有 equivalent 语义
不难理解,更强的 ordering 总是 imply 弱一些的 ordering,所以有
1 | std::strong_ordering s = std::strong_ordering::less; |
习惯用法
1 | struct Foobar { |
- 返回类型 auto 让编译器自己推导,有特殊需求也可以写清楚返回类型
- member const function,和传统实现成 friend function 不太一样
- 用
= default让编译器自动生成,生成的实现会比较每个成员
限定指定成员比较
如果比较时候需要指定成员,例如跳过 mutex 等成员,则可以
1 | struct ID { |
逐个成员手动比较
1 | std::strong_ordering operator<=>(const Person& other) const { |
甚至在好几个 member 时看起来更规整的
1 | std::strong_ordering operator<=>(const Person& other) const { |
或者老 trick,借助 std::tie()
1 | std::strong_ordering operator<=>(const Person& other) const { |
Compiler Rewrites & Synthesized Operators
如果一个类提供了 operator<=> 但是没有提供其他操作符,编译器会特殊处理这些操作符。
0x0 Rewrites operator< / operator<= / operator> / operator >=
规则如下:
后者这四个操作符的实现会直接调用
operator<=>并利用它的结果返回如果用户提供了某个操作符的实现,则编译器会跳过为其生成 rewrite-implementation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct F {
int x;
std::strong_ordering operator<=>(const F& other) const {
std::cout << "<=> called\n";
return x <=> other.x;
}
bool operator<(const F& other) const {
std::cout << "< called\n";
return x < other.x;
}
};
int main() {
F a{1}, b{2};
bool r1 = a < b; // Prints "< called" — your operator< wins
bool r2 = a > b; // Prints "<=> called" — synthesized from <=>
bool r3 = a <= b; // Prints "<=> called" — synthesized from <=>
}
0x1 Implicitly Synthesized operator== / operator!=
这两个操作符的处理稍有不同:
- 如果定义了 defaulted operator<=> 则编译器会自动合成 defaulted operator== 和 defaulted operator!=
- 反之,如果用户实现了 non-defaulted operator<=>,则不会再自动合成 == 和 != 操作符
这里是直接合成(synthesize/generate)对应的操作符,而不是基于 operator<=> 重写(rewrite)操作符,因为 operator==/operator!= 的实现对于相当一部分类型,例如容器类型,通常都有优化:只要元素个数不一样就一定不一样,不用再逐个比较元素
而大小关系的比较相对来说成本较高
0x2 Defaulted operator<=> 的要求
不当语言律师研究那些犄角旮旯,只说最常见的日常情形。
参数必须要是类型本身
1 | struct Number { |
member 里如果有不支持 operator<=> 的成员则类自身这个也会变成 deleted
Reversed Rewritten Candidates
如果一个类实现了 operator<=> 那么对于 a < b 这样的比较操作,编译器不仅会考虑 a < b 还会考虑改写成 b > a ,解决以前操作符重载作为成员函数时,无法做到对称的问题
1 | TEST_CASE("Reverse rewrite candidates") { |
Hidden Friend Impl
因为比较运算符自 C++20 支持 Reverse Rewrites Candidates,所以对比较运算符重载实现来说,大部分情况下没必要用以前的 hidden friend 方案了
除非需要比较 tricky 的 implicit conversion
1 | class Number { |
不过这种需求有点危险,一不小心容易踩坑
另外就是其他的运算符不支持 RRC,所以还是要用到 hidden friend