C++20 三方比较运算符:从 <=> 到自动生成的比较器

在 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::less
  • std::strong_ordering::equal
  • std::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 性能

  • 单一调用:只需一次比较,返回值可直接用于判断 lessequalgreater。相比多重 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<=> 与自动生成的比较器,开发者能够专注于业务逻辑,而不必纠结于重复的比较实现。无论是简单的值类,还是复杂的自定义排序,掌握这项特性都能让你的代码更现代、更易维护。

发表评论