三路比较运算符 <=>¶
| 标准 | 难度 | 状态 | 分级 | 证据 | 审阅 | 最后校验 |
|---|---|---|---|---|---|---|
| C++20 | 3/5 | final | A | 3 条(manual 3) | 单审(zyuhao7) | 2026-10-24 |
编译器支持:GCC 10+ · Clang 10+ · MSVC 19.20+。版本查询:cppreference 编译器支持。
主张与证据¶
| 主张 | 支持证据 |
|---|---|
| 为 basic_json 实现三路比较并保留无序结果 | ev-json-spaceship-basic-json-003 |
| 用 <=> 比较 JSON Pointer 的 token 序列 | ev-json-spaceship-pointer-001 |
| 按预定义顺序比较值类型,无法比较时返回 unordered | ev-json-spaceship-value-type-002 |
本站证据页:ev-json-spaceship-basic-json-003 · ev-json-spaceship-pointer-001 · ev-json-spaceship-value-type-002
解决什么问题¶
C++20 之前,一个可比较类型要写六个运算符:==、!=、<、<=、>、>=。它们容易彼此不一致:比如 < 按一个字段比较,== 按另一个字段比较,排序结果与等价判断互相矛盾。手写六个函数还意味着六处维护点,任何字段变动都要同步修改。
<=> 用一个三路比较返回“小于/等于/大于”的序关系,编译器据此重写其余关系运算符。配合 = default,成员逐一比较可以一行生成,且顺序与声明顺序一致。
基础用法¶
#include <compare>
#include <iostream>
struct Version {
int major;
int minor;
int patch;
auto operator<=>(const Version&) const = default;
};
int main() {
Version a{1, 4, 0};
Version b{1, 10, 0};
std::cout << (a < b) << '\n';
std::cout << (a == b) << '\n';
}
完整示例见 examples/cpp20/spaceship.cpp:
a <=> b 返回 std::strong_ordering;a < b 由编译器重写为 (a <=> b) < 0,a == b 由隐式声明的默认 operator== 处理。
进阶用法¶
三种序关系对应不同语义。 std::strong_ordering 表示相等即不可区分(整数、默认逐成员比较的结构体);std::weak_ordering 表示等价但可区分(大小写不敏感的字符串);std::partial_ordering 允许“无序”(浮点 NaN)。强的可以隐式转弱的,弱的可以转偏序,反向不行。
默认 <=> 会隐含默认 ==。 只写 auto operator<=>(const T&) const = default; 时,编译器同时声明默认 ==。若自己写了 ==,则不再生成。不要同时手写 <=> 和 == 却让两者比较不同字段。
<=> 的返回类型不是 bool。 返回 bool 会让 a < b 的重写语义失效,也丢失“相等/无序”的信息。自定义比较应返回三种序关系之一,或用 std::strong_order 等定制点把现有比较转成序关系。
浮点成员会把结果降为偏序。 默认逐成员比较时,只要有一个成员产生 partial_ordering(如 double),整个 <=> 就返回 partial_ordering;NaN 与任何值比较都是 unordered。用 std::is_eq、std::is_lt、std::is_lteq 等函数判断,而不是直接和 0 比较,可读性更好。
手写 <=> 要比 == 更严格。 约定是:a <=> b == 0 当且仅当 a == b。标准不强制检查,但违反约定会让排序容器与查找逻辑出现不一致。对大小写不敏感字符串,若 == 区分大小写而 <=> 不区分,应使用 weak_ordering 并让 == 与等价关系一致。
重写只发生在运算符缺失时。 如果类已经有 operator<,a < b 不会改走 <=>;如果已有 operator==,a == b 也不会由 <=> 重写。混用新旧运算符时,检查实际选中的是哪一个。
枚举与指针也可以默认比较。 枚举的 <=> 按底层值比较;指针的内置比较只保证同一数组内有序,跨对象结果未指定,需要全序时用 std::less。
真实项目证据¶
| 项目 | 位置 | 用法 | 为什么典型 |
|---|---|---|---|
| nlohmann/json | include/nlohmann/detail/json_pointer.hpp#L868-L872 |
用 <=> 比较两个 JSON Pointer 的 token 序列 |
直接委托给已有容器的三路比较,不手写六个运算符 |
| nlohmann/json | include/nlohmann/detail/value_t.hpp#L95-L100 |
按预定义顺序比较 JSON 值类型,无法比较时返回 unordered |
偏序的典型用法:类型序不构成全序 |
| nlohmann/json | include/nlohmann/json.hpp#L3745-L3754 |
为 basic_json 实现三路比较并保留无序结果 |
复杂类型的比较逻辑集中在一个入口,其余运算符由重写获得 |
三条证据均为 manual 级:作者复核,尚未引入第二名审核者。
陷阱与边界¶
- 不要返回
int或bool。 三路比较应返回序关系类型;返回bool会让<、<=等重写产生错误语义。 - 默认比较按声明顺序。 字段顺序改变会改变比较结果;把不影响身份的缓存字段放进比较会破坏等价关系。
==与<=>可能不一致。 编译器不检查两者的语义一致性;容器查找用==,排序用<=>,不一致时行为怪异。- 浮点的
unordered不是错误。NaN使比较返回unordered,a < b、a == b、a > b全为假;排序前要决定如何处理NaN。 <=>不是虚函数。 通过基类引用比较时按静态类型选择;多态比较仍需虚函数或类型擦除。- 重写有作用域限制。 成员
<=>只对同类型或可转换类型生效;异构比较需要额外重载或转换。 - 默认
<=>要求所有成员可比较。 含不可比较成员的类不能默认生成;手写时也要保证返回类型能表达所有成员的结果。 std::strong_order不是<=>的替代品。 它是定制点,用于把现有比较提升为指定序关系;不会自动为类生成运算符。
库联动¶
- nlohmann/json:
basic_json的<=>先比较类型序,再比较值;无法比较的组合返回partial_ordering::unordered。这让 JSON 值可以参与排序,同时保留“不同类型不可比”的语义。 - 与 concepts 联动:
std::three_way_comparable约束检查<=>的返回类型与一致性要求。用 concept 约束泛型排序函数,比在函数体里static_assert更早失败。 - 与
std::set/std::map联动:默认比较器仍使用std::less,它通过<工作;<=>让<自动生成,因此有序容器可以直接使用新类型。注意std::less不要求全序之外的一致性。 - 与 fmt / spdlog 联动:序关系类型本身不可直接格式化;调试输出时用
std::is_lt等转成布尔或字符串,避免打印出无意义的内部值。
横向对比¶
| 方案 | 需要实现的运算符 | 一致性 | 主要代价 |
|---|---|---|---|
<=> + = default |
1 个(== 可自动生成) |
成员顺序天然一致 | 需要 C++20;浮点成员得到偏序 |
| 手写六个运算符 | 6 个 | 容易互相矛盾 | 维护点多,字段变动易漏 |
std::tie 比较 |
每个运算符各写一次 | 字段顺序集中 | 仍需六个函数,且返回引用元组 |
compare 成员函数 |
1 个 + 六个转发 | 逻辑集中 | 样板代码仍在,调用方要记返回值约定 |
| C++20 前的宏/CRTP | 1 个宏 + 生成 | 取决于宏实现 | 可读性差,调试困难 |
跨标准演进:C++20 引入 <=>、序关系类型与运算符重写;C++23 未改变核心语义。C++26 的反射提案可能自动生成比较,但当前仍应显式声明 = default。