重载比较运算符与三路比较:C++20 的 <=> 运算符

说到比较运算符,我最早写 C++ 的时候,最烦的就是给自定义类型写 operator<operator== 这一套。你想想看,一个结构体有七八个字段,每个比较运算符都得手写一遍,不光累,还容易写错。更别提 operator!=operator== 逻辑相反这种低级 bug 了——嗯,我承认我踩过这个坑。

C++20 引入的 <=> 运算符,说白了就是来救命的。它叫“三路比较运算符”,也有人叫它“太空船运算符”(长得像飞船嘛)。它能一次性搞定所有比较逻辑,编译器还能帮你自动生成剩下的。

三路比较运算符是什么?

先看个最简单的例子:

#include <compare>
#include <iostream>

struct Point {
    int x, y;

    // C++20 三路比较
    auto operator<=>(const Point& other) const = default;
};

int main() {
    Point a{1, 2}, b{1, 3};

    if (a < b)  std::cout << "a < b\n";
    if (a == b) std::cout << "a == b\n";
    if (a != b) std::cout << "a != b\n";

    return 0;
}

看到没?只写了一个 operator<=>,而且加了 = default。编译器自动帮你生成了 <<=>>===!= 这六个运算符。我当年要是赶上 C++20,至少能省下几百行重复代码。

核心要点: 三路比较运算符返回的不是 bool,而是一个“比较类别类型”。常见的有 std::strong_orderingstd::weak_orderingstd::partial_ordering

比较类别类型

这里有个细节我得提醒你。<=> 的返回值类型决定了比较的“严格程度”。我画了张图帮你理清关系:

三路比较运算符返回值类型 std::strong_ordering 可替换相等 less / equal / greater 例:int、string std::weak_ordering 不可替换相等 less / equivalent / greater 例:大小写不敏感字符串 std::partial_ordering 允许不可比较 less / equivalent / greater / unordered 例:浮点数 NaN 严格程度递减:strong → weak → partial 实际使用建议 • 能用 strong_ordering 就用,语义最清晰 • 浮点数比较用 partial_ordering,处理 NaN 场景

我个人习惯是:能用 = default 就别手写。编译器生成的逐成员比较,按声明顺序从左到右,遇到第一个不等就返回结果。这跟结构体布局天然一致,不容易出错。

手写三路比较

当然,有时候默认行为不满足需求。比如你只想比较某些字段,或者比较逻辑有特殊规则。这时候就得手写:

struct Student {
    std::string name;
    int score;

    // 只按分数比较,忽略姓名
    std::strong_ordering operator<=>(const Student& other) const {
        if (auto cmp = score <=> other.score; cmp != 0)
            return cmp;
        return std::strong_ordering::equal;
    }
};

这里我用了 std::strong_ordering 作为返回类型。为什么不用 auto?因为我想明确告诉调用者:这个比较是“可替换相等”的——也就是说,如果两个学生分数相等,它们在逻辑上就是等价的。

小技巧: 手写 <=> 时,可以用 if (auto cmp = ...; cmp != 0) return cmp; 这种写法,逐层比较字段,最后返回 equal。这比嵌套 if-else 清爽多了。

自动生成比较运算符的规则

这里有个坑,我曾经在 code review 时抓到过好几次。编译器自动生成比较运算符是有条件的:

  • 所有成员变量都必须支持 <=> 比较
  • 如果某个成员是数组,数组元素也必须支持
  • 基类也必须支持 <=>
  • 联合体(union)不能自动生成

一旦条件不满足,= default 就会编译失败。这时候你得手写,或者给成员类型补上 <=>

注意: 如果你显式声明了 operator==operator< 中的任何一个,编译器就不会再自动生成对应的运算符了。这是设计上的“你声明了你负责”原则。

实际项目中的经验

我在一个游戏引擎项目里用过三路比较。当时有个 Transform 组件,包含位置、旋转、缩放三个向量。如果用老办法,得写 6 个运算符,每个都要比较三个向量。用 <=> 之后,一行代码搞定:

struct Transform {
    Vector3 position;
    Quaternion rotation;
    Vector3 scale;

    auto operator<=>(const Transform&) const = default;
};

而且因为 Vector3Quaternion 本身也支持 <=>,整个比较链是自动展开的。代码量从 60 行缩到了 1 行,可读性还更高了。

不过我也要提醒你:<=> 不是万能的。如果你的比较逻辑涉及外部状态(比如缓存、懒加载),或者比较结果依赖于运行时上下文,那还是老老实实手写比较函数吧。工具再好,也得看场景。

总结一下

特性 说明
<=> 运算符 三路比较,返回比较类别类型
= default 自动生成所有比较运算符
比较类别 strong / weak / partial,严格程度递减
手写场景 自定义比较逻辑、成员不支持 <=>

说实话,三路比较是我在 C++20 里最喜欢的特性之一。它把一堆琐碎的重复劳动变成了编译器的工作,让我能更专注于业务逻辑。如果你还在用 C++17 或更早的标准,我建议你尽快升级——光这一个特性就值回票价了。

公众号:蓝海资料掘金营,微信 deep3321