Day15-C++20 三路比较运算符 `<=>`:一次定义,全面排序

C++20 三路比较运算符 <=>:一次定义,全面排序

C++进阶计划 · Day 15 | 预计学习时长:2小时

引言

为什么这个知识点重要

在 C++20 之前,实现一个完整的可比较类型是一项繁琐的工作。你需要手写 6 个比较运算符(==!=<<=>>=),每一个都要保证逻辑一致性。这不仅冗长,还容易出错------比如 <> 的定义不一致,导致排序算法行为异常。

C++20 引入了三路比较运算符 (spaceship operator,<=>),一举解决了这个问题。它不仅是语法糖,更是对"比较"这一概念的重新设计------将排序类别(ordering categories)引入类型系统,让编译器帮你生成所有比较运算符。

与前面内容的关联

  • Day 09 学习了 C++20 Concepts,三路比较与 std::totally_orderedstd::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 ⟺ 不可区分 所有值可比 整数、字符串 lessequalgreater
weak_ordering a == b 不等价于不可区分 所有值可比 忽略大小写的字符串比较 lessequivalentgreater
partial_ordering 等价性同上 存在不可比的值 浮点数(NaN) lessequalgreaterunordered

强序(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;
};

最佳实践清单

  1. 优先使用 = default:除非有特殊需求,让编译器生成默认比较
  2. 保持 ==<=> 一致 :自定义 == 时必须同步考虑 <=>
  3. 选择最弱的排序类别:不要为了"方便"把弱序强转为强序
  4. <=> 应与 == 一起定义<=> 不等于 ==,不要用 < 0 替代 ==
  5. 在 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 兼容性仍是挑战

参考资源


下一节预告:Day 16 我们将探索 C++23 的最新特性,看看哪些对 Qt 开发者最有实用价值。

相关推荐
A_cainiao_A7 天前
C++20:std::stop_source让线程说停就听
开发语言·c++20
Billy121387 天前
Day10-C++20 Ranges(上):惰性求值与组合式数据处理
开发语言·c++进阶学习
小小龙学IT11 天前
C++20 Ranges 库实现原理深度解析:从视图适配器到管道运算符
c++20
Billy1213815 天前
C++20 Concepts:约束与概念的现代范式
c++20·c++进阶学习
可爱系程序猿15 天前
mfc90.dll 缺失如何处理?VC++ 2008 运行库与并行程序集排查记录
程序人生·电脑·c++20
code_pgf16 天前
C++11 / C++14 / C++17 / C++20 新特性总结
c++·c++20
C语言Plus17 天前
C++ SqlBuilder一个简单、灵活且类型安全的 C++ SQL 构建器库
数据库·sql·安全·c++20
Billy1213821 天前
结构化绑定与 if constexpr
开发语言·c++进阶学习
CHANG_THE_WORLD1 个月前
C++20 协程从零入门:用 ASCII 图彻底理解 `co_await`、协程帧与异步执行
c++20