C++20 三路比较运算符 <=>:一次定义,全面排序
C++进阶计划 · Day 15 | 预计学习时长:2小时
引言
为什么这个知识点重要
在 C++20 之前,实现一个完整的可比较类型是一项繁琐的工作。你需要手写 6 个比较运算符(==、!=、<、<=、>、>=),每一个都要保证逻辑一致性。这不仅冗长,还容易出错------比如 < 和 > 的定义不一致,导致排序算法行为异常。
C++20 引入了三路比较运算符 (spaceship operator,<=>),一举解决了这个问题。它不仅是语法糖,更是对"比较"这一概念的重新设计------将排序类别(ordering categories)引入类型系统,让编译器帮你生成所有比较运算符。
与前面内容的关联
- Day 09 学习了 C++20 Concepts,三路比较与
std::totally_ordered、std::three_way_comparable等 concept 紧密相关 - Day 10-11 的 Ranges 排序算法依赖比较运算符,三路比较让自定义类型的排序更简洁
- 本日是 C++20 特性的深化,为 Day 16 的 C++23 新特性做铺垫
核心概念
1. 三种排序类别
C++20 定义了三种排序类别(ordering categories),它们形成了一个层次结构:
std::strong_ordering (强序)
↑ 可隐式转换
std::weak_ordering (弱序)
↑ 可隐式转换
std::partial_ordering (偏序)
| 排序类别 | 等价性 | 可比性 | 典型场景 | 可能的值 |
|---|---|---|---|---|
strong_ordering |
a == b ⟺ 不可区分 |
所有值可比 | 整数、字符串 | less、equal、greater |
weak_ordering |
a == b 不等价于不可区分 |
所有值可比 | 忽略大小写的字符串比较 | less、equivalent、greater |
partial_ordering |
等价性同上 | 存在不可比的值 | 浮点数(NaN) | less、equal、greater、unordered |
强序(strong_ordering) :a == b 意味着 f(a) == f(b) 对任何函数 f 都成立。换句话说,相等的值完全不可区分。例如整数 3 == 3,它们在一切上下文中表现相同。
弱序(weak_ordering) :a == b(或 a equivalent b)并不意味着不可区分。例如忽略大小写比较时 CaseInsensitiveString{"abc"} == CaseInsensitiveString{"ABC"},但它们在大小写敏感的场景中显然不同。
偏序(partial_ordering) :存在不可比较的值。最典型的例子是浮点数------NaN 与任何值(包括自身)都不满足 <、==、> 中的任何一个。
2. 比较类别的底层设计
cpp
#include <compare>
#include <iostream>
int main() {
// 三个比较类别的枚举值
std::strong_ordering so = 3 <=> 5;
std::cout << (so == std::strong_ordering::less) << "\n"; // 1
std::cout << (so < 0) << "\n"; // 1(与0比较)
std::cout << (so == std::strong_ordering::equal) << "\n"; // 0
std::weak_ordering wo = std::weak_ordering::equivalent;
std::partial_ordering po = std::partial_ordering::unordered;
// partial_ordering 独有的 unordered 状态
std::cout << (po == std::partial_ordering::unordered) << "\n"; // 1
// 强序可隐式转换为弱序和偏序
std::weak_ordering wo2 = so; // OK: 强序 → 弱序
std::partial_ordering po2 = so; // OK: 强序 → 偏序
std::partial_ordering po3 = wo; // OK: 弱序 → 偏序
// 反向转换不允许:
// std::strong_ordering so2 = wo; // 编译错误!
// std::weak_ordering wo3 = po; // 编译错误!
return 0;
}
3. 默认比较运算符的自动生成
C++20 的核心能力:你只需要定义 <=> 和 ==,编译器自动生成其余四个。
更进一步,<=> 还可以用 = default 让编译器自动合成:
cpp
#include <compare>
#include <string>
class Person {
public:
std::string name;
int age;
double salary;
// 编译器自动生成成员逐一比较的三路比较
// 返回类型取决于成员中最"弱"的比较类别
// string → strong_ordering, int → strong_ordering, double → partial_ordering
// 因此这里返回 partial_ordering
auto operator<=>(const Person&) const = default;
// ⚠️ 注意:= default 的 operator<=> 会自动生成 operator==,但前提是你没有显式声明 operator==。
//以下情况需要特别注意:
//如果你自定义了 operator==,编译器不会再自动生成它,<=> 也不会帮你生成
//如果你只定义了 operator<=> 而没有定义 operator==,编译器会生成 operator==(a, b) 作为 a <=> b == 0
//如果你想自定义 == 的行为(例如忽略某些字段),你必须自己定义 operator==,但同时要确保它与 <=> 的语义一致
};
int main() {
Person a{"Alice", 30, 50000.0};
Person b{"Bob", 25, 60000.0};
// 所有 6 个比较运算符都可用
bool r1 = a < b; // 先比 name,"Alice" < "Bob" → true
bool r2 = a == b; // false
bool r3 = a >= b; // false(因为 name 已经决定了 <)
(void)r1; (void)r2; (void)r3;
return 0;
}
4. 自定义 == 与默认 <=> 的配合
一个常见场景:你希望相等性判断忽略某些字段,但排序仍然包含它们:
cpp
#include <compare>
#include <string>
#include <iostream>
class UserAccount {
public:
int id;
std::string username;
std::string last_login; // 排序时考虑,但判等时忽略
// 自定义相等性:只比较 id 和 username
bool operator==(const UserAccount& other) const {
return id == other.id && username == other.username;
}
// 默认排序:包含所有字段(成员逐一比较)
auto operator<=>(const UserAccount&) const = default;
};
int main() {
UserAccount a{1, "alice", "2024-01-01"};
UserAccount b{1, "alice", "2024-06-15"};
std::cout << std::boolalpha;
// ⚠️ 实际运行结果:
// a == b: true(忽略 last_login)
// a < b: true(比较所有字段,包括 last_login)
// 🚨 严重问题:a == b 为 true 但 a < b 也为 true
// 这违反了 C++ 标准库对比较运算符的基本要求:等价性传递性
// 这种行为会导致:
// 1. std::sort 可能产生未定义行为(要求 strict weak ordering)
// 2. std::map 等关联容器可能出错
// 3. 违反 std::equality_comparable 和 std::totally_ordered concept
// ✅ 正确做法:保持一致
// 要么两者都忽略 last_login,要么两者都包含 last_login
// 因此 default <=> 与自定义 == 混用时需注意语义
return 0;
}
代码实战
实战一:为自定义类型实现完整比较体系
cpp
#include <compare>
#include <string>
#include <vector>
#include <algorithm>
#include <iostream>
// 版本号类:典型的多字段比较场景
class Version {
public:
int major;
int minor;
int patch;
// 默认三路比较:按 major → minor → patch 顺序比较
auto operator<=>(const Version&) const = default;
};
// 忽略大小写的字符串包装
struct CaseInsensitiveString {
std::string value;
// 自定义弱序比较
std::weak_ordering operator<=>(const CaseInsensitiveString& other) const {
auto to_lower = [](const std::string& s) {
std::string result = s;
std::transform(result.begin(), result.end(), result.begin(),
[](unsigned char c) { return std::tolower(c); });
return result;
};
std::string a = to_lower(value);
std::string b = to_lower(other.value);
if (a < b) return std::weak_ordering::less;
if (a > b) return std::weak_ordering::greater;
return std::weak_ordering::equivalent;
}
bool operator==(const CaseInsensitiveString& other) const {
return (*this <=> other) == 0;
}
};
int main() {
// Version 排序
std::vector<Version> versions = {
{2, 1, 0}, {1, 9, 3}, {2, 0, 5}, {1, 0, 0}, {2, 1, 0}
};
std::sort(versions.begin(), versions.end());
std::cout << "Sorted versions:\n";
for (const auto& v : versions) {
std::cout << " " << v.major << "." << v.minor << "." << v.patch << "\n";
}
// 查找
auto it = std::find(versions.begin(), versions.end(), Version{2, 0, 5});
// ✅ OK:operator== 由 default <=> 自动生成
if (it != versions.end()) {
std::cout << "Found: " << it->major << "." << it->minor << "." << it->patch << "\n";
}
// 弱序比较
CaseInsensitiveString a{"Hello"};
CaseInsensitiveString b{"hello"};
CaseInsensitiveString c{"World"};
std::cout << "\nCase-insensitive:\n";
std::cout << std::boolalpha;
std::cout << "\"Hello\" == \"hello\": " << (a == b) << "\n"; // true
std::cout << "\"Hello\" < \"World\": " << (a < c) << "\n"; // true
std::cout << "\"hello\" <=> \"HELLO\": equivalent = "
<< ((a <=> CaseInsensitiveString{"HELLO"}) == 0) << "\n"; // true
return 0;
}
实战二:Qt 结合 ------ QVariant 的排序与三路比较
Qt 的 QVariant 本身不支持直接的比较运算符。在实际开发中,我们经常需要对包含 QVariant 的模型数据进行排序。C++20 的三路比较可以让我们优雅地实现这一点。
cpp
#include <compare>
#include <variant>
#include <string>
#include <vector>
#include <algorithm>
#include <iostream>
// 模拟 QVariant 的行为:一个能持有多种类型的值容器
// Qt 的 QVariant 使用 type() + 内部存储,我们用 std::variant 近似
using VariantValue = std::variant<int, double, std::string>;
// 为 VariantValue 实现偏序比较
// 为什么是偏序而非强序?因为不同类型之间无法直接比较(类比 NaN 的 unordered)
std::partial_ordering compare_variants(const VariantValue& a, const VariantValue& b) {
// 类型不同时返回 unordered
if (a.index() != b.index()) {
return std::partial_ordering::unordered;
}
return std::visit([&](const auto& va) -> std::partial_ordering {
using T = std::decay_t<decltype(va)>;
const auto& vb = std::get<T>(b);
if (va < vb) return std::partial_ordering::less;
if (va > vb) return std::partial_ordering::greater;
return std::partial_ordering::equivalent;
}, a);
}
// 包装成支持三路比较的类
class SortableVariant {
public:
VariantValue value;
std::string key; // 用于同类型不可比时的后备排序(类似 Qt 模型中的列号)
std::partial_ordering operator<=>(const SortableVariant& other) const {
auto cmp = compare_variants(value, other.value);
if (cmp != std::partial_ordering::unordered) {
return cmp;
}
// 不同类型时,按 type index 排序(提供全序,但语义上是人为约定)
if (value.index() < other.value.index()) return std::partial_ordering::less;
if (value.index() > other.value.index()) return std::partial_ordering::greater;
return std::partial_ordering::equivalent;
}
bool operator==(const SortableVariant& other) const {
// ⚠️ 注意:std::variant 默认没有 operator==
// 需要包含 <variant> 且使用 C++20(std::variant 的 == 在 C++20 中可用)
// 如果编译器版本较低,需要手动实现
return value == other.value; // C++20 可用
}
// C++17 兼容的手动实现:
bool operator==(const SortableVariant& other) const {
if (value.index() != other.value.index()) return false;
return std::visit([&](const auto& v) {
return v == std::get<std::decay_t<decltype(v)>>(other.value);
}, value);
}
};
int main() {
std::vector<SortableVariant> data = {
{std::string("Banana"), "fruit"},
{42, "answer"},
{3.14, "pi"},
{std::string("Apple"), "fruit"},
{7, "lucky"},
{2.71, "e"},
};
// 排序顺序取决于 MetaType 枚举的顺序:
// MetaType::Int(0) < MetaType::Double(1) < MetaType::String(2)
// 同类型按值排序
// 输出顺序:int(42), int(7), double(2.71), double(3.14), string("Apple"), string("Banana")
std::sort(data.begin(), data.end());
std::cout << "Sorted variants:\n";
for (const auto& item : data) {
std::visit([](const auto& v) {
using T = std::decay_t<decltype(v)>;
if constexpr (std::is_same_v<T, int>)
std::cout << " int: " << v << "\n";
else if constexpr (std::is_same_v<T, double>)
std::cout << " double: " << v << "\n";
else
std::cout << " string: " << v << "\n";
}, item.value);
}
return 0;
}
实战三:Qt 模型排序代理中的三路比较
cpp
// 以下代码展示如何在 QSortFilterProxyModel 的 lessThan 中使用三路比较
// 这是伪代码/概念代码,展示设计思路
/*
// 在 Qt 6 中,你可以这样利用三路比较简化排序逻辑:
class SortableItem {
public:
QVariant data;
int displayRole;
// 将 QVariant 的比较映射到三路比较
std::partial_ordering operator<=>(const SortableItem& other) const {
// 利用 QVariant::typeId() 和 convert() 实现类型感知的比较
if (data.typeId() == other.data.typeId()) {
// 同类型:直接比较
switch (data.typeId()) {
case QMetaType::Int:
return data.toInt() <=> other.data.toInt();
case QMetaType::Double:
return data.toDouble() <=> other.data.toDouble();
case QMetaType::QString:
return data.toString() <=> other.data.toString();
default:
return std::partial_ordering::unordered;
}
}
// 跨类型:按类型ID排序
return static_cast<int>(data.typeId()) <=> static_cast<int>(other.data.typeId());
}
bool operator==(const SortableItem& other) const {
return data == other.data;
}
};
// 在 QSortFilterProxyModel 子类中:
class MySortProxy : public QSortFilterProxyModel {
protected:
bool lessThan(const QModelIndex& left, const QModelIndex& right) const override {
SortableItem a{sourceModel()->data(left), left.column()};
SortableItem b{sourceModel()->data(right), right.column()};
auto result = a <=> b;
return result == std::partial_ordering::less;
}
};
*/
#include <compare>
#include <iostream>
// 简化版演示:用枚举模拟 QVariant::typeId()
enum class MetaType { Int, Double, String, Invalid };
struct SortableItem {
MetaType type;
int intVal = 0;
double doubleVal = 0.0;
std::string strVal;
std::partial_ordering operator<=>(const SortableItem& other) const {
if (type == other.type) {
switch (type) {
case MetaType::Int:
return intVal <=> other.intVal;
case MetaType::Double:
return doubleVal <=> other.doubleVal;
case MetaType::String:
return strVal <=> other.strVal;
default:
return std::partial_ordering::equivalent;
}
}
// 不同类型按 type enum 排序
return static_cast<int>(type) <=> static_cast<int>(other.type);
}
bool operator==(const SortableItem& other) const {
if (type != other.type) return false;
switch (type) {
case MetaType::Int: return intVal == other.intVal;
case MetaType::Double: return doubleVal == other.doubleVal;
case MetaType::String: return strVal == other.strVal;
default: return true;
}
}
};
int main() {
// ⚠️ 注意:指定初始化器(.intVal = 42)是 C++20 语法
// 如果使用 C++17,需要显式构造:
SortableItem a{MetaType::Int, 42, 0.0, {}};
SortableItem b{MetaType::Int, 17, 0.0, {}};
// 或者添加辅助构造函数:
struct SortableItem {
// ...
SortableItem(MetaType t, int v) : type(t), intVal(v) {}
SortableItem(MetaType t, double v) : type(t), doubleVal(v) {}
SortableItem(MetaType t, const std::string& v) : type(t), strVal(v) {}
};
SortableItem c{MetaType::String, .strVal = "hello"};
std::cout << std::boolalpha;
std::cout << "42 <=> 17: " << ((a <=> b) > 0) << " (greater)\n";
std::cout << "42 < 17: " << (a < b) << "\n";
// 这里 int < string 是因为 MetaType::Int(0) < MetaType::String(2)
// 这是人为约定的排序规则,不是类型本身的自然比较
std::cout << "int <=> string: "
<< ((a <=> c) == std::partial_ordering::less) << " (less, int enum < string enum)\n";
return 0;
}
常见陷阱与最佳实践
陷阱 1:默认 <=> 与自定义 == 的不一致
cpp
struct Bad {
int x;
double y;
// 自定义 == 只比较 x
bool operator==(const Bad& other) const { return x == other.x; }
// 默认 <=> 比较 x 和 y
auto operator<=>(const Bad&) const = default;
// 问题:a == b 为 true,但 a < b 可能也为 true!
// 这违反了强序的等价性公理
};
最佳实践 :如果你自定义了 ==,确保它与 <=> 的等价性语义一致。如果 == 忽略了某些字段,<=> 也应该忽略它们。
cpp
struct Good {
int x;
double y;
bool operator==(const Good& other) const { return x == other.x; }
// 自定义 <=> 也只比较 x,与 == 保持一致
// 返回 strong_ordering 是安全的,因为只比较 int
std::strong_ordering operator<=>(const Good& other) const {
return x <=> other.x;
}
// 注意:y 在排序中被忽略了,但 != 仍然由 == 生成
// 如果希望 y 影响排序,需要将 y 加入 <=> 的比较
};
陷阱 2:浮点数的 unordered 状态
cpp
#include <compare>
#include <cmath>
#include <limits>
void check_nan_comparison() {
double nan = std::numeric_limits<double>::quiet_NaN();
auto result = nan <=> 1.0;
// result 是 partial_ordering::unordered
// 这意味着 <, ==, > 全部返回 false!
if (result == std::partial_ordering::unordered) {
// 必须单独处理 unordered 情况
}
// 错误做法:假设三路比较一定返回 less/equal/greater
// if (result < 0) { ... } // NaN 比较时不会进入任何分支
}
最佳实践 :处理浮点数比较结果时,始终考虑 unordered 的可能性。
陷阱 3:返回类型推导的意外
cpp
struct WithDouble {
int x;
double y; // double 的比较是 partial_ordering
// default <=> 的返回类型是 partial_ordering(因为 double)
auto operator<=>(const WithDouble&) const = default;
};
struct WithoutDouble {
int x;
int y;
// default <=> 的返回类型是 strong_ordering(所有成员都是 strong)
auto operator<=>(const WithoutDouble&) const = default;
};
最佳实践:如果类型中包含浮点成员但你确定不会出现 NaN,可以显式指定返回类型:
cpp
struct NoNaNDouble {
int x;
double y;
std::strong_ordering operator<=>(const NoNaNDouble& other) const {
// 首先比较 x
if (auto cmp = x <=> other.x; cmp != 0) {
return cmp; // 直接返回 cmp,类型为 std::strong_ordering
}
// 假设 y 不是 NaN(调用者需要保证)
// ⚠️ 如果 y 是 NaN,以下比较会返回 false,导致错误的 equal
if (y < other.y) return std::strong_ordering::less;
if (y > other.y) return std::strong_ordering::greater;
return std::strong_ordering::equal;
}
// 更安全的做法:使用 std::strong_ordering 需要确保所有比较都是强序
// 如果 y 可能为 NaN,应该使用 std::partial_ordering
bool operator==(const NoNaNDouble& other) const = default;
};
最佳实践清单
- 优先使用
= default:除非有特殊需求,让编译器生成默认比较 - 保持
==和<=>一致 :自定义==时必须同步考虑<=> - 选择最弱的排序类别:不要为了"方便"把弱序强转为强序
<=>应与==一起定义 :<=>不等于==,不要用< 0替代==- 在 Qt 中使用
qCompare辅助函数:Qt 6.4+ 已开始支持三路比较
进阶思考
与 Concepts 的结合
C++20 的 Concepts 中有与比较相关的 concept:
cpp
#include <concepts>
#include <compare>
// std::three_way_comparable<T> --- T 支持三路比较
// std::totally_ordered<T> --- T 支持 ==, !=, <, <=, >, >=
// std::equality_comparable<T> --- T 支持 ==, !=
template<std::three_way_comparable T>
void sort_if_possible(std::vector<T>& vec) {
std::sort(vec.begin(), vec.end());
}
// 约束:只有支持三路比较的类型才能传入
排序类别与数学中的全序/偏序
三路比较的设计直接映射了数学中的序理论(Order Theory):
-
strong_ordering→ 全序(Total Order)+ 可替换性 -
weak_ordering→ 全预序(Total Preorder),即满足传递性但不一定满足反对称性的关系 -
partial_ordering→偏序(Partial Order),允许不可比较的元素补充说明:
更准确的数学映射:
strong_ordering → 全序(Total Order)+ 可替换性(substitutability)
weak_ordering → 全序(Total Order)但不保证可替换性,在数学上对应严格弱序(Strict Weak Ordering)
partial_ordering → 偏序(Partial Order),存在不可比较的元素(如浮点数中的 NaN)
Qt 中的排序演进
| Qt 版本 | 排序支持 | 说明 |
|---|---|---|
| Qt 5 | operator< 手动实现 |
需要手写所有比较运算符 |
| Qt 6.0-6.3 | operator< + 算法 |
QSortFilterProxyModel 依赖 lessThan |
| Qt 6.4+ | 三路比较(实验性) | 部分 Qt 类型开始支持 <=>,但 Q_OBJECT 类仍有 moc 限制 |
| Qt 6.7+ | 三路比较(预览) | 更多容器类型支持 <=>,但 QObject 派生类仍受限 |
| Qt 7(预期) | 全面三路比较 | 预计全面采用 C++20 比较体系,但 moc 兼容性仍是挑战 |
参考资源
- C++ Reference: Three-way comparison
- C++ Reference: Comparison categories
- P0515R3 - Consistent comparison --- 三路比较的原始提案
- C++ Weekly: The Spaceship Operator --- Jason Turner 的视频讲解
- Qt 6 and C++20 ---需注意:该博客发表于 Qt 6.0 发布时,三路比较的实际支持情况请参考 Qt 6.4+ 的发布说明
- Barry Revzin 的博客文章 Comparisons in C++20 --- 深入解析比较类别设计
下一节预告:Day 16 我们将探索 C++23 的最新特性,看看哪些对 Qt 开发者最有实用价值。