在 C++20 里,三路比较运算符()为自定义类型的比较提供了极致的便利与一致性。与传统的 <、<=、== 等多重比较操作符相比, 只需要实现一次即可得到所有必要的关系运算符,并且能自动生成稳定、可组合的比较结果。本文从语法、返回类型、默认化比较、与浮点数的特殊处理,以及在标准库容器中的应用等角度,全面剖析 Spaceship 的使用技巧与最佳实践。
1. 语法与基本用法
struct Person {
std::string name;
int age;
// 三路比较运算符
std::strong_ordering operator<=>(const Person&) const = default;
// 或者手动实现:
// std::strong_ordering operator<=>(const Person& rhs) const {
// if (age != rhs.age) return age <=> rhs.age;
// return name <=> rhs.name;
// }
};
std::strong_ordering、std::weak_ordering、std::partial_ordering分别表示强、弱、部分排序的返回类型。强排序意味着所有值都能比较;弱排序用于不稳定排序或可比较但无总序的情况;部分排序适用于 NaN、无穷大等特殊浮点值的比较。operator<=>(const Person&) const = default;让编译器自动根据成员顺序生成三路比较实现。
结果:编译器会为 Person 自动生成 ==, !=, <, <=, >, >= 等关系运算符,全部基于 <=> 的返回值。
2. 返回类型细节
| 返回类型 | 说明 | 典型场景 |
|---|---|---|
std::strong_ordering |
完全可比较,所有值都有总序 | 整数、字符串、指针等无缺失值类型 |
std::weak_ordering |
仅保证三相关系,可能存在等价但不严格的 | 复合键、多维度比较 |
std::partial_ordering |
支持部分排序,某些值不可比较 | 浮点数(NaN 与非 NaN 的比较) |
如果想要返回自定义的比较结果(比如返回 int),需要自行定义对应的比较类型或手动实现 operator<=> 并返回相应的类型。
3. 默认化比较的强大之处
struct Point {
int x, y;
std::strong_ordering operator<=>(const Point&) const = default;
};
- 只要所有成员都支持
<=>(或者==、<等),编译器即可为Point生成完整的比较函数。 - 在 STL 容器中使用
Point,如 `std::set `, `std::map`,无需手动写比较器。
注意:默认化比较会按成员声明顺序进行 lexicographic 比较。若业务逻辑需要自定义顺序,必须手动实现 operator<=>(const Point&)。
4. 与浮点数的比较:std::partial_ordering 的妙用
struct Measurement {
double value;
// 采用 partial_ordering,以正确处理 NaN
std::partial_ordering operator<=>(const Measurement&) const = default;
};
- 当
value为NaN时,任何比较均返回unordered,从而避免 NaN 被误认为小于或大于其他数。 - 对于需要强排序的场景,可以在比较前手动对 NaN 做处理或使用
std::isnan判断。
5. 自定义排序与自定义返回值
5.1 强制自定义排序规则
struct Student {
std::string name;
double gpa;
// 先按 GPA 降序,再按姓名升序
std::weak_ordering operator<=>(const Student& rhs) const {
if (auto cmp = gpa <=> rhs.gpa; cmp != 0) return cmp;
return name <=> rhs.name;
}
};
5.2 返回 int 或其他类型
如果你想返回 int(比如 1、0、-1),可以手动实现:
int operator<=>(const Person& rhs) const {
if (age != rhs.age) return age < rhs.age ? -1 : 1;
if (name != rhs.name) return name < rhs.name ? -1 : 1;
return 0;
}
但请注意,这种做法会失去 <=> 的自动推断优势,不推荐在 C++20 标准库的上下文中使用。
6. 在标准容器中的实际使用
std::set <Person> people; // 自动使用默认比较
people.insert({"Alice", 30});
people.insert({"Bob", 25});
for (const auto& p : people) {
std::cout << p.name << " (" << p.age << ")\n";
}
如果需要自定义排序:
struct PersonCmp {
bool operator()(const Person& a, const Person& b) const {
return a.age < b.age || (a.age == b.age && a.name < b.name);
}
};
std::set<Person, PersonCmp> peopleByAge;
但在多数情况下,= default 已能满足需求,减少了手写比较器的工作量。
7. 与传统 operator< 的兼容性
-
对于已有项目,若仅需要添加
<=>,可保留原有operator<,然后让<=>调用operator<:bool operator<(const Person& rhs) const { return name < rhs.name; } auto operator<=>(const Person& rhs) const { return (*this < rhs) ? std::strong_ordering::less : (*this > rhs) ? std::strong_ordering::greater : std::strong_ordering::equal; } -
或者直接
= default;并让编译器生成所有关系运算符,兼容旧代码。
8. 性能与编译器支持
<=>由编译器在编译阶段完成,产生的代码与手写比较几乎无差异。- 对于
std::default_sentinel_t或std::optional等类型,C++20 已提供对应的<=>实现。 - 大多数主流编译器(GCC 10+, Clang 11+, MSVC 19.28+)均已完整实现三路比较。
9. 小结
- 三路比较 通过单一接口统一所有关系运算,显著降低代码冗余。
- 默认化 让编译器根据成员顺序自动生成比较器,极大提升开发效率。
- 返回类型 的灵活性满足强排序、弱排序以及部分排序需求,尤其在浮点数比较中展现独特优势。
- 自定义实现 能满足复杂业务规则,且不会与标准库冲突。
在日益复杂的 C++ 项目中,掌握 <=> 的使用不只是一个技术细节,更是一种提升代码质量、可读性和维护性的策略。下次在实现自定义类型时,记得先考虑三路比较,或许你会发现自己写了两行代码,却得到完整、可组合、符合现代 C++ 风格的比较功能。