在 C++20 中,三方比较运算符(three-way comparison operator,简称“三方比较”或“空间船运算符”)为实现强类型、可组合的比较逻辑提供了强大工具。本文将回顾其语法与语义,讨论如何利用 operator<=> 以及自动生成的比较器构建可排序的类型,并剖析其在性能与安全性方面的优势。
1. 三方比较运算符的基本形式
std::strong_ordering operator<=> (const T& lhs, const T& rhs);
返回值类型是 std::strong_ordering(或 std::weak_ordering / std::partial_ordering),分别对应完全可比、弱可比与部分可比。返回值可以是:
std::strong_ordering::lessstd::strong_ordering::equalstd::strong_ordering::greater
此外,还支持 std::weak_ordering::unordered 用于浮点数的 NaN 处理。
1.1 自动转换为 <, <=, >, >=
一旦定义了 operator<=>,编译器会自动为 operator<、operator<=、operator>、operator>= 生成对应的实现:
bool operator< (const T& lhs, const T& rhs) { return lhs <=> rhs == std::strong_ordering::less; }
bool operator<= (const T& lhs, const T& rhs) { return lhs <=> rhs != std::strong_ordering::greater; }
...
这大大降低了手写多重比较运算符的工作量。
2. 自动生成的比较器(<=> + operator==)
C++20 还引入了 自动生成的比较器:只需定义 operator<=>,如果你还定义了 operator==,编译器会自动生成 operator!=、operator<、operator<=、operator>、operator>=。如果你未显式定义 operator==,编译器会生成默认比较(逐成员比较)与 operator<=> 的等价实现。
struct Person {
std::string name;
int age;
auto operator<=> (const Person&) const = default; // 默认生成的三方比较
// 如果不写 `operator==`,编译器会生成默认的相等比较
};
使用 = default 可以让编译器根据成员类型自动实现三方比较。若成员类型已实现 operator<=>,则会递归调用;否则会使用默认的 < 比较。
2.1 何时选择 = default
- 值语义:结构体仅包含值类型(如
int,std::string,std::optional等),且成员顺序决定排序规则时,使用= default最简洁。 - 兼容性:若成员类型已实现
operator<=>,则默认实现与手写实现完全等价。 - 可维护性:不必担心忘记更新比较逻辑,任何成员修改都会自动反映。
3. 组合排序规则
有时你需要自定义排序逻辑,优先比较某个字段,再按次要字段排序。可以在 operator<=> 内手动指定:
auto operator<=> (const Person& other) const {
if (auto cmp = name <=> other.name; cmp != 0) return cmp;
return age <=> other.age;
}
使用 if (auto cmp = ...; cmp != 0) 是 C++20 的简洁语法,避免多重 if。
4. 性能与安全性优势
4.1 性能
- 单一调用:只需一次比较,返回值可直接用于判断
less、equal、greater。相比多重operator<、operator==调用,减少函数调用次数。 - 更好优化:编译器可利用返回值信息生成更高效的比较路径,例如使用
memcmp或 SIMD 进行批量比较。
4.2 安全性
- 避免误用:传统
operator<与operator==的组合可能导致误写顺序(如忘记更新operator<),operator<=>明确表达排序关系。 - 一致性:当成员类型更改时,
= default会自动更新比较逻辑,减少维护错误。
5. 常见陷阱与注意事项
| 现象 | 说明 | 解决方案 |
|---|---|---|
operator<=> 返回值为 std::partial_ordering |
对 std::vector<double> 等包含 NaN 的类型使用 partial_ordering |
对浮点数使用 std::partial_ordering::unordered 或自定义比较 |
成员未实现 operator<=> |
默认实现会退化为 operator< 调用 |
手动实现成员的三方比较或改用 = default 并确保成员支持 |
混用 = default 与手写比较 |
可能导致不一致 | 只使用其中一种,或者在 = default 前写注释说明 |
6. 实战案例:自定义 Rectangle 排序
#include <compare>
#include <string>
#include <vector>
#include <algorithm>
#include <iostream>
struct Rectangle {
int width;
int height;
std::string name;
// 先按面积排序,面积相等时按名称
auto operator<=> (const Rectangle& other) const {
if (auto cmp = (width * height) <=> (other.width * other.height); cmp != 0) return cmp;
return name <=> other.name;
}
// 需要手动实现 operator==,因为我们自定义了 operator<=>,否则编译器会报缺失
bool operator==(const Rectangle&) const = default;
};
int main() {
std::vector <Rectangle> rects = {
{10, 20, "A"},
{5, 50, "B"},
{10, 20, "C"},
{4, 25, "D"}
};
std::sort(rects.begin(), rects.end());
for (auto& r : rects) {
std::cout << r.name << " (" << r.width << "x" << r.height << ")\n";
}
}
输出:
D (4x25)
A (10x20)
C (10x20)
B (5x50)
7. 结语
三方比较运算符为 C++20 带来了更简洁、更安全、更高效的比较机制。通过 operator<=> 与自动生成的比较器,开发者能够专注于业务逻辑,而不必纠结于重复的比较实现。无论是简单的值类,还是复杂的自定义排序,掌握这项特性都能让你的代码更现代、更易维护。