28、C++20 三路比较运算符:<=> 运算符详解、自动生成比较运算符、与手写比较的对比
三路比较运算符,也就是 <=>,是 C++20 引入的一个重磅特性。我第一次看到这个符号时,第一反应是「这什么鬼?飞船吗?」——没错,它确实有个外号叫「飞船运算符」(spaceship operator)。
但别被它的外表骗了。这东西不是花架子,它实实在在地解决了 C++ 中一个老大难问题:写比较运算符太烦了。
为什么需要三路比较?
你想想看,一个普通的类,如果要支持排序、查找、放进 std::set 或 std::map,你得写多少个运算符?==、!=、<、<=、>、>=,一共六个。而且它们之间还有逻辑关系:a != b 等价于 !(a == b),a <= b 等价于 !(b < a)。
我在项目中遇到过好几次,团队成员手写比较运算符时漏写了 <=,结果编译通过,运行时排序行为诡异。排查了半天才发现是某个比较没定义。嗯,这种坑踩过一次就记住了。
三路比较运算符的核心思想很简单:你只需要定义一次「比较逻辑」,编译器就能自动帮你生成剩下的五个运算符。
<=> 的基本用法
先看一个最简单的例子:
#include <iostream>
#include <compare>
struct Point {
int x;
int y;
// 自动生成所有比较运算符
auto operator<=>(const Point&) const = default;
};
int main() {
Point p1{1, 2};
Point p2{1, 3};
std::cout << std::boolalpha;
std::cout << (p1 < p2) << '\n'; // true
std::cout << (p1 == p2) << '\n'; // false
std::cout << (p1 != p2) << '\n'; // true
std::cout << (p1 >= p2) << '\n'; // false
}
看到没?只写了一行 auto operator<=>(const Point&) const = default;,六个比较运算符全有了。这就是自动生成的威力。
返回类型:三种比较类别
<=> 的返回值不是 bool,而是一个「比较类别」类型。C++20 定义了三种:
| 比较类别 | 含义 | 适用场景 |
|---|---|---|
std::strong_ordering |
可替换相等(equal 等价于 identical) | 整数、字符串等值类型 |
std::weak_ordering |
不可替换相等(equal 不等价于 identical) | 大小写不敏感字符串、有理数约分 |
std::partial_ordering |
允许不可比较(unordered) | 浮点数(NaN 场景) |
说白了,strong_ordering 是最严格的:如果 a == b,那么 a 和 b 在任何场景下都不可区分。weak_ordering 则允许「相等但不同」的情况。而 partial_ordering 还允许「无法比较」的结果。
举个例子:
#include <compare>
// 强序:整数比较
static_assert((1 <=> 2) == std::strong_ordering::less);
static_assert((1 <=> 1) == std::strong_ordering::equal);
// 偏序:浮点数比较
auto r = 0.0 <=> std::numeric_limits<double>::quiet_NaN();
// r 是 std::partial_ordering::unordered
我记得有一次在代码审查时,有人用 double 作为 std::map 的 key。我当时就提醒他:浮点数比较是偏序,NaN 会导致 unordered 结果,map 的行为会变得不可预测。后来他改用 strong_ordering 的自定义比较器才解决问题。
自动生成比较运算符的规则
当你写 auto operator<=>(const T&) const = default; 时,编译器会:
- 按声明顺序逐个比较每个基类子对象
- 按声明顺序逐个比较每个非静态数据成员
- 使用每个成员的
<=>运算符进行比较 - 推导出整体的比较类别(取所有成员中最弱的那个)
这里有个细节:如果所有成员都是 strong_ordering,整体就是 strong_ordering;只要有一个是 weak_ordering,整体就降级为 weak_ordering。
重要提示: = default 的 <=> 会自动生成 == 和 !=。但如果你只自定义了 <=> 而没有 = default,编译器不会自动生成 == 和 !=。这是 C++20 的一个设计决策——因为自定义的 <=> 可能不满足「相等即不可区分」的语义。
手写比较 vs 自动生成:实战对比
咱们来看一个真实场景。假设有一个 Person 类:
struct Person {
std::string name;
int age;
double height;
};
手写版本(C++17 及之前):
struct Person {
std::string name;
int age;
double height;
// 六个运算符,一个都不能少
bool operator==(const Person& other) const {
return name == other.name &&
age == other.age &&
height == other.height;
}
bool operator!=(const Person& other) const {
return !(*this == other);
}
bool operator<(const Person& other) const {
if (name != other.name) return name < other.name;
if (age != other.age) return age < other.age;
return height < other.height;
}
bool operator<=(const Person& other) const {
return !(other < *this);
}
bool operator>(const Person& other) const {
return other < *this;
}
bool operator>=(const Person& other) const {
return !(*this < other);
}
};
三路比较版本(C++20):
struct Person {
std::string name;
int age;
double height;
// 一行搞定
auto operator<=>(const Person&) const = default;
};
差别一目了然。手写版本 30 行,自动生成版本 1 行。而且手写版本容易出错——比如 <= 的逻辑写反了,或者某个成员漏掉了。我见过最离谱的 bug 是有人把 age 和 height 的比较顺序搞反了,导致排序结果完全不对。
我的建议: 如果你的类只是简单的数据聚合(POD 或类似 POD),直接用 = default 的 <=>。只有在需要自定义比较逻辑时,才手写 <=> 运算符。
自定义三路比较:更灵活的控制
有时候默认的比较逻辑不满足需求。比如,你想让 Person 只按 age 比较,忽略 name 和 height:
struct Person {
std::string name;
int age;
double height;
std::strong_ordering operator<=>(const Person& other) const {
return age <=> other.age;
}
// 注意:自定义 <=> 不会自动生成 == 和 !=
bool operator==(const Person& other) const {
return age == other.age;
}
};
这里有个坑:自定义 <=> 后,编译器不会自动生成 == 和 !=。你必须自己写 operator==。为什么?因为编译器无法保证你的 <=> 语义中「相等」和「等价」是一回事。
我曾经在项目中犯过这个错:自定义了 <=> 但忘了写 ==,结果 std::find 一直找不到元素。排查了半天才发现是 == 没定义,编译器用了默认的按位比较。嗯,这种教训一次就够了。
性能对比:自动生成 vs 手写
很多人担心自动生成的性能不如手写。其实大可不必。编译器对 = default 的 <=> 会做激进的优化,生成的代码和手写版本几乎没有区别。
我做过一个简单的 benchmark:
- 手写版本:每个比较运算符单独实现
- 自动生成版本:
= default的<=> - 混合版本:手写
<=>但让编译器生成其他运算符
结果三个版本在 -O2 下的汇编代码几乎一模一样。所以性能不是问题,放心用。
注意: 如果你的类包含指针成员,= default 的 <=> 会比较指针的值(地址),而不是指针指向的内容。这种情况下,你必须手写比较逻辑。
知识体系总览
下面这张图总结了本章的核心知识点:
避坑指南
最后,分享几个我踩过的坑:
- 指针成员要小心: 默认比较的是指针地址,不是指向的内容。如果你用
std::unique_ptr或原始指针,记得手写比较逻辑。 - 浮点数成员: 如果类里有
float或double,自动生成的<=>会返回partial_ordering。这意味着a == b可能为false即使数值上相等(因为有 NaN)。 - 继承体系: 如果基类定义了
<=>,派生类用= default时会自动包含基类的比较。但要注意,如果基类的<=>是自定义的,派生类不会自动生成==。 - 不要混用: 不要在一个类里同时手写部分比较运算符又用
= default的<=>。这会导致语义混乱,编译器也可能报错。
三路比较运算符是 C++20 最实用的特性之一。它减少了大量样板代码,降低了出错概率。我个人现在写新代码时,只要类需要比较,第一反应就是 auto operator<=>(const T&) const = default;。除非有特殊需求,否则绝不手写那六个运算符。
记住:让编译器帮你干活,你才有时间做更有意义的事情。