C++20 中的三路比较运算符(Spaceship)如何彻底简化排序与比较?

在 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_orderingstd::weak_orderingstd::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;
};
  • valueNaN 时,任何比较均返回 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_tstd::optional 等类型,C++20 已提供对应的 <=> 实现。
  • 大多数主流编译器(GCC 10+, Clang 11+, MSVC 19.28+)均已完整实现三路比较。

9. 小结

  • 三路比较 通过单一接口统一所有关系运算,显著降低代码冗余。
  • 默认化 让编译器根据成员顺序自动生成比较器,极大提升开发效率。
  • 返回类型 的灵活性满足强排序、弱排序以及部分排序需求,尤其在浮点数比较中展现独特优势。
  • 自定义实现 能满足复杂业务规则,且不会与标准库冲突。

在日益复杂的 C++ 项目中,掌握 <=> 的使用不只是一个技术细节,更是一种提升代码质量、可读性和维护性的策略。下次在实现自定义类型时,记得先考虑三路比较,或许你会发现自己写了两行代码,却得到完整、可组合、符合现代 C++ 风格的比较功能。

发表评论