跳到主要内容

C++ 20三路比较

引言

在C++20之前,如果我们想要比较两个对象的大小关系,通常需要实现六种比较运算符:==!=<<=>>=。这不仅工作量大,而且容易出错,特别是在确保各个运算符行为一致性方面。C++20引入了三路比较运算符(也称为"太空船运算符")<=>,它可以帮助我们大幅简化比较操作的实现,并确保比较行为的一致性。

什么是三路比较运算符?

三路比较运算符 <=> 的名称来源于其外观,它看起来像一艘太空船,因此也被称为"太空船运算符"(spaceship operator)。

与传统的比较运算符不同,三路比较运算符不仅能判断两个值是否相等,还能判断它们的大小关系。它返回三种可能的结果:

  • 小于零 - 表示左操作数小于右操作数
  • 等于零 - 表示左操作数等于右操作数
  • 大于零 - 表示左操作数大于右操作数

三路比较运算符的返回类型是一个比较类别类型,主要有:

  • std::strong_ordering
  • std::weak_ordering
  • std::partial_ordering

基本用法

下面是一个简单的例子,展示三路比较运算符的基本用法:

cpp
#include <iostream>
#include <compare>

int main() {
int a = 5;
int b = 10;

auto result = a <=> b;

if (result < 0) {
std::cout << "a < b" << std::endl;
} else if (result == 0) {
std::cout << "a == b" << std::endl;
} else { // result > 0
std::cout << "a > b" << std::endl;
}

return 0;
}

输出:

a < b

比较类别类型

C++20引入了三种比较类别类型,用于表示不同类型的比较结果:

1. std::strong_ordering

表示全序比较,包含三个值:lessequalgreater。这是最强的比较类型,适用于整数、枚举等类型。

cpp
std::strong_ordering::less    // <
std::strong_ordering::equal // ==
std::strong_ordering::greater // >

2. std::weak_ordering

表示弱序比较,也包含三个值:lessequivalentgreater。适用于区分相等性(equality)和等价性(equivalence)的情况,如不区分大小写的字符串比较。

cpp
std::weak_ordering::less        // <
std::weak_ordering::equivalent // ==
std::weak_ordering::greater // >

3. std::partial_ordering

表示偏序比较,增加了一个额外的值:unordered。适用于存在不可比较值的类型,如浮点数(考虑NaN的情况)。

cpp
std::partial_ordering::less      // <
std::partial_ordering::equivalent // ==
std::partial_ordering::greater // >
std::partial_ordering::unordered // 无法比较

默认比较

C++20还引入了默认比较功能,可以通过添加= default来自动生成比较运算符:

cpp
#include <iostream>
#include <compare>

struct Point {
int x;
int y;

// 自动生成三路比较运算符
auto operator<=>(const Point&) const = default;
};

int main() {
Point p1{1, 2};
Point p2{1, 3};

if (p1 < p2) {
std::cout << "p1 < p2" << std::endl;
} else {
std::cout << "p1 >= p2" << std::endl;
}

return 0;
}

输出:

p1 < p2
备注

默认生成的三路比较运算符会按声明顺序逐个比较类的成员。在上面的例子中,首先比较x,如果相等,再比较y

自定义比较方式

除了使用默认比较外,我们也可以自定义比较逻辑:

cpp
#include <iostream>
#include <compare>
#include <string>

class Person {
private:
std::string name;
int age;

public:
Person(std::string n, int a) : name(std::move(n)), age(a) {}

// 自定义比较:按年龄比较
std::strong_ordering operator<=>(const Person& other) const {
return age <=> other.age;
}

// 还需要自定义相等比较
bool operator==(const Person& other) const {
return age == other.age;
}

const std::string& getName() const { return name; }
int getAge() const { return age; }
};

int main() {
Person p1{"Alice", 30};
Person p2{"Bob", 25};

if (p1 > p2) {
std::cout << p1.getName() << " is older than " << p2.getName() << std::endl;
} else if (p1 < p2) {
std::cout << p1.getName() << " is younger than " << p2.getName() << std::endl;
} else {
std::cout << p1.getName() << " is the same age as " << p2.getName() << std::endl;
}

return 0;
}

输出:

Alice is older than Bob
警告

请注意,当自定义<=>运算符时,C++20不会自动生成==运算符。如果需要相等比较功能,你仍然需要自己实现==运算符。

三路比较与其他比较运算符的关系

在C++20中,当你定义了三路比较运算符<=>和相等运算符==后,C++编译器会自动为你生成其他四个比较运算符(!=<<=>>=)。这大大减少了编写代码的工作量,并确保了各个运算符之间的一致性。

下面的代码展示了此功能的工作原理:

cpp
#include <iostream>
#include <compare>

struct Value {
int value;

// 只需定义这两个运算符
auto operator<=>(const Value&) const = default;
bool operator==(const Value&) const = default;

// 编译器会自动生成:
// bool operator!=(const Value&) const
// bool operator<(const Value&) const
// bool operator<=(const Value&) const
// bool operator>(const Value&) const
// bool operator>=(const Value&) const
};

int main() {
Value v1{10};
Value v2{20};

if (v1 != v2) std::cout << "v1 != v2" << std::endl;
if (v1 < v2) std::cout << "v1 < v2" << std::endl;
if (v1 <= v2) std::cout << "v1 <= v2" << std::endl;
if (v2 > v1) std::cout << "v2 > v1" << std::endl;
if (v2 >= v1) std::cout << "v2 >= v1" << std::endl;

return 0;
}

输出:

v1 != v2
v1 < v2
v1 <= v2
v2 > v1
v2 >= v1

实际应用场景

1. 自定义类的排序

三路比较运算符特别适合实现自定义类的排序功能:

cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <compare>

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

// 首先按分数降序排序,分数相同时按姓名升序排序
std::strong_ordering operator<=>(const Student& other) const {
if (auto cmp = other.score <=> score; cmp != 0)
return cmp;
return name <=> other.name;
}

bool operator==(const Student& other) const = default;
};

int main() {
std::vector<Student> students = {
{"Alice", 85},
{"Bob", 90},
{"Charlie", 85},
{"David", 95}
};

std::sort(students.begin(), students.end());

for (const auto& student : students) {
std::cout << student.name << ": " << student.score << std::endl;
}

return 0;
}

输出:

David: 95
Bob: 90
Alice: 85
Charlie: 85

2. 实现版本号比较

三路比较运算符可以轻松实现版本号的比较逻辑:

cpp
#include <iostream>
#include <compare>
#include <vector>

class Version {
private:
std::vector<int> segments;

public:
Version(std::initializer_list<int> segs) : segments(segs) {}

std::strong_ordering operator<=>(const Version& other) const {
// 按位比较版本号
for (size_t i = 0; i < std::min(segments.size(), other.segments.size()); ++i) {
if (auto cmp = segments[i] <=> other.segments[i]; cmp != 0) {
return cmp;
}
}

// 处理不同长度的版本号
return segments.size() <=> other.segments.size();
}

bool operator==(const Version& other) const = default;

// 显示版本号
friend std::ostream& operator<<(std::ostream& os, const Version& v) {
for (size_t i = 0; i < v.segments.size(); ++i) {
if (i > 0) os << ".";
os << v.segments[i];
}
return os;
}
};

int main() {
Version v1{1, 2, 3};
Version v2{1, 3, 0};
Version v3{1, 2, 3, 0};

std::cout << v1 << " < " << v2 << ": " << (v1 < v2 ? "true" : "false") << std::endl;
std::cout << v1 << " < " << v3 << ": " << (v1 < v3 ? "true" : "false") << std::endl;
std::cout << v2 << " < " << v3 << ": " << (v2 < v3 ? "true" : "false") << std::endl;

return 0;
}

输出:

1.2.3 < 1.3.0: true
1.2.3 < 1.2.3.0: true
1.3.0 < 1.2.3.0: false

浮点数比较与 NaN 处理

使用三路比较运算符比较浮点数时,需要特别注意 NaN(Not a Number)的处理:

cpp
#include <iostream>
#include <compare>
#include <cmath>

int main() {
double a = 1.0;
double b = 2.0;
double nan = std::numeric_limits<double>::quiet_NaN();

auto result1 = a <=> b;
std::cout << "1.0 <=> 2.0: "
<< (result1 < 0 ? "less" : result1 == 0 ? "equal" : "greater")
<< std::endl;

auto result2 = a <=> nan;
std::cout << "1.0 <=> NaN: ";
if (result2 < 0) std::cout << "less";
else if (result2 == 0) std::cout << "equal";
else if (result2 > 0) std::cout << "greater";
else std::cout << "unordered";
std::cout << std::endl;

// 检测是否可比较
if (std::isunordered(a, nan)) {
std::cout << "a and nan are unordered" << std::endl;
}

return 0;
}

输出:

1.0 <=> 2.0: less
1.0 <=> NaN: unordered
a and nan are unordered
注意

当涉及 NaN 的比较时,结果通常是"无序的"(unordered)。在处理可能包含 NaN 的浮点数据时,务必要小心。

总结

C++20 的三路比较运算符(<=>)是一项强大的语言特性,它可以:

  1. 简化比较运算符的实现
  2. 确保比较操作的一致性
  3. 通过默认实现轻松处理结构体和类的比较
  4. 提供更细粒度的比较结果类型

使用三路比较运算符,可以显著减少编写比较运算符所需的代码量,并降低出错的可能性。特别是在处理复杂数据结构的排序和比较时,三路比较运算符能够带来更简洁、更可靠的代码。

练习

  1. 创建一个 Rectangle 类,包含宽度和高度,并使用三路比较运算符实现按面积比较。
  2. 实现一个 IPAddress 类,使用三路比较运算符比较 IPv4 地址。
  3. 为一个含有多个成员的结构体实现自定义比较逻辑,使用三路比较运算符。
  4. 尝试在代码中使用 std::partial_ordering 处理包含 NaN 的浮点数比较。

扩展阅读

  • C++20 标准文档中关于三路比较运算符的部分
  • 关于比较类别类型的更多细节,可以阅读 <compare> 头文件的文档
  • 探索 C++20 中与三路比较运算符协同工作的其他新特性,如默认函数模板参数

通过深入理解和使用三路比较运算符,你可以编写出更简洁、更健壮的比较代码,这是 C++20 为现代 C++ 编程带来的重要改进之一。