在C++中,"<=>"("spaceship",三路比较)运算符是什么?

302
在我试图了解C++运算符的过程中,我偶然发现了一个列出了一个奇怪比较运算符的表格。这个<=>运算符是做什么用的?
自2017年以来,cppreference.com更新了该页面,现在包含了关于<=>运算符的详细信息

1
@cubuspl42 bar< foo::operator<=> 是一个示例,说明它可能像 <-- 运算符一样。 - Yakk - Adam Nevraumont
10
@haccks说:对的,比如C++11是关于实现C++11的编译器的标签,而C++14是关于实现C++14的编译器的标签,C++17是关于实现C++17的编译器的标签。不,C++20是关于C++20的东西的标签。既然这个问题是关于C++20的,那就是它了。错误的是标签描述,而不是标签本身。 - Nicol Bolas
请查看[tag:c++-faq]中的此答案 - undefined
6个回答

223

这被称为三路比较运算符

根据P0515文件提案:

有一个新的三路比较运算符,<=>。表达式a <=> b返回一个对象,如果a < b,则比较结果为<0,如果a > b,则比较结果为>0,如果ab相等/等价,则比较结果为==0

要为类型编写所有比较,请只需编写返回适当类别类型的operator<=>:

  • 如果您的类型自然支持<,则返回_ordering,我们将高效生成<><=>===!=;否则返回_equality,我们将高效生成==!=

  • 如果对于您的类型,a == b意味着f(a) == f(b)(可替换性,其中f仅读取使用非私有const接口访问的比较相关状态),则返回strong;否则返回weak

cppreference说:

三路比较运算符表达式的形式为:

lhs <=> rhs   (1)  

该表达式返回一个对象,

  • 如果 lhs < rhs,则比较结果为 <0
  • 如果 lhs > rhs,则比较结果为 >0
  • 如果 lhsrhs 相等或等效,则比较结果为 ==0

190
对于那些像我一样困惑的人,不知道“compares <0>”,“compares >0”和“compares ==0”是什么意思,它们意味着<=>根据参数返回负数、正数或零值。就像strncmpmemcmp一样。 - Cornstalks
1
@Dai 即使 'a' < 'a''c' < 'a' 都是 false,但是 'a' < 'a''a' < 'c' 不是。在强排序中,以下语句成立:a != ba < b || b < a - Revolver_Ocelot
1
@Revolver_Ocelot 噢,那么它可以被定义/生成为 operator==(T x, T y) { return !(x < y) && !(y < x); }operator!=(T x, T y) { return (x < y) || (y < x); } - 啊哈!当然这比真正的 == 不太高效,因为它会两次调用比较,但仍然很不错。 - Dai
5
“return strong”和“return weak”的意思是什么? - lucidbrot
2
@hkBattousai 这意味着当对象与 < 0 进行比较时,它会返回。也就是说,如果 a < b,那么 (a <=> b) < 0 总是为真。 - rmobis
显示剩余5条评论

160
在2017年11月11日,ISO C++委员会采纳了Herb Sutter的提案,将"<=>"(太空船)三路比较运算符作为C++20的新特性之一。在题为"Consistent comparison"的论文中,Sutter、Maurer和Brown展示了新设计的概念。以下是文章的摘录,对提案进行了概述:
表达式 a <=> b 返回一个对象,如果 a < b,则比较结果为 <0;如果 a > b,则比较结果为 >0;如果 a 和 b 相等/等价,则比较结果为 ==0
常见情况:要为类型 X 与类型 Y 进行所有比较,并采用成员逐个比较的语义,只需编写以下代码:
auto X::operator<=>(const Y&) =default;

高级情况:要为类型 X 与类型 Y 进行所有比较,只需编写接受 Y 参数的 operator<=>,如果需要,可以使用 =default 来获得成员逐个比较的语义,并返回适当的类别类型:
- 如果您的类型自然支持 <,则返回 _ordering,我们将高效生成对称的 <><=>===!=; - 否则返回 _equality,我们将高效生成对称的 ==!=
- 如果对于您的类型 a == b 意味着 f(a) == f(b)(可替换性,其中 f 仅读取使用公共 const 成员访问的比较相关状态),则返回 strong_; - 否则返回 weak_
比较类别

定义了五个比较类别,分别为std::类型,具有以下预定义值:

┌──────────────────┬───────────────────────────────────┬─────────────┐
│                  │          Numeric  values          │ Non-numeric │
│     Category     ├──────┬────────────┬───────────────┤             │
│                  │ -1   │ 0          │ +1            │   values    │
├──────────────────┼──────┼────────────┼───────────────┼─────────────┤
│ strong_ordering  │ less │ equal      │ greater       │             │
│ weak_ordering    │ less │ equivalent │ greater       │             │
│ partial_ordering │ less │ equivalent │ greater       │ unordered   │
│ strong_equality  │      │ equal      │ nonequal      │             │
│ weak_equality    │      │ equivalent │ nonequivalent │             │
└──────────────────┴──────┴────────────┴───────────────┴─────────────┘

这些类型之间的隐式转换定义如下:
  • strong_ordering 的值为 {less, equal, greater} 会隐式转换为:
    • weak_ordering 的值为 {less, equivalent, greater}
    • partial_ordering 的值为 {less, equivalent, greater}
    • strong_equality 的值为 {unequal, equal, unequal}
    • weak_equality 的值为 {nonequivalent, equivalent, nonequivalent}
  • weak_ordering 的值为 {less, equivalent, greater} 会隐式转换为:
    • partial_ordering 的值为 {less, equivalent, greater}
    • weak_equality 的值为 {nonequivalent, equivalent, nonequivalent}
  • partial_ordering 的值为 {less, equivalent, greater, unordered} 会隐式转换为:
    • weak_equality 的值为 {nonequivalent, equivalent, nonequivalent, nonequivalent}
  • strong_equality 的值为 {equal, unequal} 会隐式转换为:
    • weak_equality 的值为 {equivalent, nonequivalent}
三路比较
引入了<=>标记。字符序列<=>在旧的源代码中被标记为<= >。例如,X<&Y::operator<=>需要添加一个空格以保留其含义。
可重载的运算符<=>是一个三路比较函数,其优先级高于<,低于<<。它返回一个可以与字面值0进行比较的类型,但也允许其他返回类型,以支持表达式模板。语言和标准库中定义的所有<=>运算符都返回上述5种std::比较类别类型之一。
对于语言类型,提供了以下内置的<=>相同类型比较。除非另有说明,所有这些比较都是constexpr的。这些比较不能使用标量提升/转换进行异构调用。
对于bool、整数和指针类型,<=>返回strong_ordering。
对于指针类型,允许不同的cv限定和派生到基类的转换来调用同质内置<=>,并且有内置的异质operator<=>(T*,nullptr_t)。只有指向相同对象/分配的指针比较是常量表达式。
对于基本浮点类型,<=>返回partial_ordering,并且可以通过将参数扩大到较大的浮点类型来异质地调用。
对于枚举,<=>返回与枚举的基础类型的<=>相同的类型。
对于nullptr_t,<=>返回strong_ordering,并且始终产生equal。
对于可复制的数组,T[N] <=> T[N]返回与T的<=>相同的类型,并执行逐元素的词典比较。其他数组没有<=>。
对于void没有<=>。
为了更好地理解这个运算符的内部工作原理,请阅读原始的论文。这只是我使用搜索引擎找到的一些信息。另外,请查看比较运算符,包括三路比较(C++20)

5
好像C++已经够复杂了,为什么不直接编写一个比较方法呢? - Leandro
14
太空船操作符就是那个比较方法。此外,它能正常工作,并编写(或删除)其他六个比较运算符。与编写六个单独的模板相比,我更喜欢编写一个比较运算符函数。 - jonspaceharper
4
请注意,_equality 类型已经废弃:事实证明 <=> 与四个关系运算符配合得很好,但与两个相等运算符配合得不太好(尽管有一些强大的语法糖支持常见情况下同时使用所有运算符)。 - Davis Herring

27
默认情况下,使用<=>操作符会自动获得==, !=, <, >, <=, >=操作符的功能。
C++20引入了一个新的“默认比较”功能,使得默认情况下使用<=>操作符会自动获得其他所有操作符的功能。我相信这是添加operator<=>的主要动机。
来源:https://en.cppreference.com/w/cpp/language/default_comparisons(经过修改)

main.cpp

#include <cassert>
#include <compare>
#include <set>

struct Point {
    int x;
    int y;
    auto operator<=>(const Point&) const = default;
};

int main() {
    Point pt1{1, 1}, pt2{1, 2};

    // Just to show it Is enough for `std::set`.
    std::set<Point> s;
    s.insert(pt1);

    // All of these are automatically defined for us!
    assert(!(pt1 == pt2));
    assert( (pt1 != pt2));
    assert( (pt1 <  pt2));
    assert( (pt1 <= pt2));
    assert(!(pt1 >  pt2));
    assert(!(pt1 >= pt2));
}

编译并运行:
sudo apt install g++-10
g++-10 -ggdb3 -O0 -std=c++20 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out

一个更明确的等效版本如下所示:
struct Point {
    int x;
    int y;
    auto operator<=>(const Point& other) const {
        if (auto cmp = x <=> other.x; cmp != 0)
            return cmp;
        return y <=> other.y;
    }
    bool operator==(const Point& other) const = default;
};

在这种情况下,我们需要明确设置bool operator==(const Point& other) const = default;,因为如果operator<=>没有默认值(例如上面明确给出的情况),那么operator==就不会自动默认:
根据任何operator<=>重载的规则,一个默认的<=>重载也将允许使用<<=>>=来比较类型。
如果operator<=>被默认化且根本没有声明operator==,那么operator==将被隐式默认化。
上面的示例使用与默认operator<=>相同的算法,如cppreference所解释的那样:
默认的`operator<=>`通过逐个比较T的基类(从左到右,深度优先)和非静态成员(按声明顺序)子对象来执行词典比较,以计算`<=>`,递归地展开数组成员(按递增下标顺序),并在找到不相等的结果时提前停止。
在C++20之前,你不能像`operator== = default`这样做,并且定义一个操作符不会导致其他操作符被定义,例如以下代码在`-std=c++17`下无法编译通过:
#include <cassert>

struct Point {
    int x;
    int y;
    auto operator==(const Point& other) const {
        return x == other.x && y == other.y;
    };
};

int main() {
    Point pt1{1, 1}, pt2{1, 2};

    // Do some checks.
    assert(!(pt1 == pt2));
    assert( (pt1 != pt2));
}

出现错误:
main.cpp:16:18: error: no match for ‘operator!=’ (operand types are ‘Point’ and ‘Point’)
   16 |     assert( (pt1 != pt2));
      |              ~~~ ^~ ~~~
      |              |      |
      |              Point  Point

上述代码在`-std=c++20`下是可以编译通过的。
相关链接:
- [C++运算符重载是否会基于其他运算符自动提供?](link1) - [为什么应该使用三路比较运算符(<=>)而不是两路比较运算符?这有什么优势吗?](link2)
`<=>`的返回类型:`std::strong_ordering` vs `std::weak_ordering` vs `std::partial_ordering`
`<=>`的返回类型不是一个`int`(-1, 0, 1),而是几种类型之一的对象,可以与`int`进行比较,并提供关于实现的排序类型的进一步信息。

当我们进行了实现时:

    auto operator<=>(const Point& other) const {
        if (auto cmp = x <=> other.x; cmp != 0)
            return cmp;
        return y <=> other.y;
    }

auto会给我们返回std::strong_ordering,因为这是<=>在两个整数之间返回的结果,正如在https://en.cppreference.com/w/cpp/language/operator_comparison中提到的。

否则,如果操作数具有整数类型,则该运算符产生一个类型为std::strong_ordering的prvalue。

这就是我们对整数点所期望的语义。

当在浮点数之间使用<=>时,会得到partial_ordering,因为存在"不可比较"的NaN的可能性。

TODO 具体的weak_orderingpartial_ordering的示例。

Practical meaning of std::strong_ordering and std::weak_ordering表明,在标准库中目前没有它们产生不同结果的示例。

相关:PartialOrdering, StrictWeakOrdering, TotalOrdering, 在应用中的主要区别是什么 在Ubuntu 20.04、GCC 10.2.0上进行了测试。

1
当不使用默认 <=> 时,它是否应该同时给出 ==? 对我来说,它会重载 ><,但是会提示缺少 == 运算符... 使用默认设置时就没有问题。 - Tony
1
@TonyTannous,根据https://en.cppreference.com/w/cpp/language/default_comparisons的规定,这似乎是预期的行为,“根据任何operator<=>重载的规则,默认的<=>重载也将允许比较类型的<、<=、>和>=。如果operator<=>被默认,并且operator==根本没有声明,则operator==将隐式默认。”解决方案是像我的一个示例那样添加 bool operator==(const Point& other) const = default; - Ciro Santilli OurBigBook.com
是的,我也在p1185r2中找到了这个背后的动机 - Tony
1
<=> 应该返回一些比较类别(在您的例子中,std::strong_ordering 是有意义的)。如果将其返回类型设为 int,则无法进行组合。 - undefined
@JanSchultke 谢谢,我不知道这个。我更新了,试图修复它。"non-composable" 是什么意思?你能给一个例子的链接吗? - undefined
1
如果你在这个例子(https://godbolt.org/z/9fYoY3jKc)中将`T`改为`int`,它将无法编译通过。你需要使用比较类别,以便编译器能够为包含的类型组合一个默认的`<=>std::common_comparison_categoryint`上不起作用。 - undefined

23

C++ 20引入了三路比较运算符(<=>)。

该表达式返回以下对象;

auto cmp  = a <=> b;

cmp > 0 if a > b
cmp = 0 if a == b
cmp < 0 if a < b  

示例程序

#include <iostream>

using namespace std;

int main()
{
        int lhs = 10, rhs = 20;
        auto result = lhs <=> rhs;

        if (result < 0) {
                cout << "lhs is less than rhs" << endl;
        }
        else if (result > 0) {
                cout << "lhs is greater than rhs" << endl;
        }
        else {
                cout << "lhs and rhs are equal" << endl;
        }

}

如何编译和运行?

g++-10 threewaycmp.cpp -std=c++20
./a.out

结果

lhs is less than rhs
请参考以下链接以获取更多详细信息: https://en.cppreference.com/w/cpp/language/operator_comparison

2
哇。这是最被低估的答案了。简单而且明了。 - Niclas
1
@Niclas 是的...这是非常简洁的答案!! - African_king

11

由于引用的网页已更改,此答案已失效

您所引用的网页已经失效。当时正在对该网页进行编辑,不同部分之间没有同步。 我查看时的状态如下:

在页面顶部,它列出了当前存在的比较运算符(在C++14中)。那里没有<=>

在页面底部,它们应该列出相同的运算符,但出现了未来的建议。

gcc目前还不知道<=>(使用-std=c++14, 将永远不会有),所以它认为你意思是 a <= > b。这解释了错误消息的原因。

如果你五年后尝试同样的事情,你可能会得到更好的错误信息,比如<=>不是C++14的一部分。


1
OP链接的网页是正确的,你链接的单独页面也是正确的。它使用(自C++20起)标签来限定<=>运算符,告诉你可以期望在哪个版本的标准中找到它。标准标签是cppreference.com遵循的惯例。当然,你没有一个时光机器回来支持它的编译器,但cpprefernce会(正确地)告诉你可以期望什么。 - Spencer
是的,但是...这不是一个答案。你在评论...或者其他什么。 - q-l-p
2
我本意是要链接到与问题相同的网页,但是错过了。我认为我回答了其他答案没有回答到的部分。我忽略了主要的加粗问题,因为其他人已经回答了它。 - Stig Hemmer
这个回答根本没有试图回答问题“<=> 运算符是什么?”它更适合作为对问题的评论。 - undefined

0

<=> 的优点在于对于比较昂贵的复杂类型,例如树形导航等情况。您可以执行一次 int x = A <=> B;,然后从 x 确定 ><>=<===!=,而无需进行 A 和 B 的其他比较。对于树形结构,想象一下 bool find(root, A){ if root is nullptr return false; int x = A <=> root->B; if !x, you found it return true, else if x < 0, find left, else find right}。(调整尾递归。)

我认为某些语言具有三路控制流,例如 switch,但很难通过 Google 回忆起来。这是一个涉及 C strcmp()、memcmp(),并继续使用 JAVA compareTo() 的情况。请帮我众包!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接