C ++ 20添加了一个称为“太空飞船”的新运算符:
<=>
。 不久前,
Simon Brand发布了一篇
帖子 ,其中包含有关此运算符是什么以及其用途的详细概念信息。 这篇文章的主要任务是研究“奇怪的”新运算符及其类似
operator==
的特定应用,并为在日常编码中的使用提出一些建议。
比较方式
看到如下代码并不罕见:
struct IntWrapper { int value; constexpr IntWrapper(int value): value{value} { } bool operator==(const IntWrapper& rhs) const { return value == rhs.value; } bool operator!=(const IntWrapper& rhs) const { return !(*this == rhs); } bool operator<(const IntWrapper& rhs) const { return value < rhs.value; } bool operator<=(const IntWrapper& rhs) const { return !(rhs < *this); } bool operator>(const IntWrapper& rhs) const { return rhs < *this; } bool operator>=(const IntWrapper& rhs) const { return !(*this < rhs); } };
注意:细心的读者会注意到,这实际上比在C ++ 20之前的代码中的冗长程度要小得多。 稍后再详细介绍。您需要编写许多标准代码,以确保我们的类型可与相同类型的东西进行比较。 好的,我们待会儿解决。 然后是这样写的人:
constexpr bool is_lt(const IntWrapper& a, const IntWrapper& b) { return a < b; } int main() { static_assert(is_lt(0, 1)); }
您会注意到的第一件事是程序将无法编译。
error C3615: constexpr function 'is_lt' cannot result in a constant expression
问题是比较函数中忘记了
constexpr
。 然后一些会将
constexpr
添加到所有比较运算符。 几天后,有人将添加
is_gt
,但是请注意,所有比较运算符都没有异常说明,并且您将必须经历相同的繁琐过程,
noexcept
5个重载中的每一个都添加
noexcept
。
这就是新的C ++ 20飞船操作员向我们提供帮助的地方。 让我们看看如何在C ++ 20世界中编写原始的
IntWrapper
:
#include <compare> struct IntWrapper { int value; constexpr IntWrapper(int value): value{value} { } auto operator<=>(const IntWrapper&) const = default; };
您可能会注意到的第一个区别是
<compare>
的新添加。
<compare>
头负责为编译器填充飞船运算符所需的所有类型的比较类别,以便它返回适合于我们的默认函数的类型。 在以上代码段中,
auto
的返回类型将为
std::strong_ordering
。
我们不仅删除了5条额外的行,而且甚至不需要确定任何内容,编译器会为我们完成。
is_lt
保持不变,并且在保持
constexpr
不变的
is_lt
仍可以正常工作,尽管我们没有在默认的
operator<=>
明确指定。 这样做很好,但是有些人可能会
is_lt
为什么即使根本不使用宇宙飞船运算符
is_lt
可以允许
is_lt
进行编译。 让我们找到这个问题的答案。
重写表达式
在C ++ 20中,将编译器引入与“重写”表达式有关的新概念。 宇宙飞船算子以及
operator==
是可重写的前两个候选对象之一。 有关重写表达式的更具体示例,让我们看一下
is_lt
给出的
is_lt
。
解决重载时,编译器将从一组最合适的候选项中进行选择,每个候选项均对应于我们所需的运算符。 当比较器和等效操作时,选择器的选择过程将略有变化,此时编译器还必须收集特殊的转录和合成候选对象(
[over.match.oper] /3.4 )。
对于我们的表达式
a < b
标准指出我们可以为接受此类型的
operator<=>
或
operator<=>
寻找类型
a
。 这就是编译器的工作,发现
a
类型实际上包含
IntWrapper::operator<=>
。 然后允许编译器使用此运算符,并将表达式
a < b
重写为
(a <=> b) < 0
。 然后,此重写的表达式将用作正常重载解决方案的候选。
您可能会问为什么这个重写的表达式正确。 表达式的正确性实际上源于太空飞船操作员提供的语义。
<=>
是一种三向比较,这意味着您不仅得到二进制结果,而且还得到顺序(在大多数情况下)。 如果您有订单,则可以根据任何比较操作来表达此订单。 一个简单的示例,C ++ 20中的表达式4 <=> 5将返回结果
std::strong_ordering::less
。
std::strong_ordering::less
的结果表示
4
不仅不同于
5
而且严格小于此值,这使得操作
(4 <=> 5) < 0
正确且准确地描述了我们的结果。
使用以上信息,编译器可以采用任何广义比较运算符(即
<
,
>
等),并根据飞船运算符对其进行重写。 在标准中,重写的表达式通常称为
(a <=> b) @ 0
,其中
@
表示任何比较操作。
合成表达
读者可能已经注意到上面对“合成”表达式的细微引用,并且它们在重写语句的过程中也发挥了作用。 考虑以下功能:
constexpr bool is_gt_42(const IntWrapper& a) { return 42 < a; }
如果我们将原始定义用于
IntWrapper
,则此代码将无法编译。
error C2677: binary '<': no global operator found which takes type 'const IntWrapper' (or there is no acceptable conversion)
这在C ++ 20之前是有意义的,并且解决此问题的方法是向
IntWrapper
添加一些其他函数,
IntWrapper
函数占据
int
的左侧。 如果尝试使用编译器和
IntWrapper
C ++ 20定义来构建此示例,则可能会再次注意到它只是有效。 让我们看看为什么上面的代码仍在C ++ 20中编译。
解决重载时,编译器还将收集标准所谓的“综合”候选对象,或使用参数相反顺序重写的表达式。 在上面的示例中,编译器将尝试使用重写的表达式
(42 <=> a) < 0
,但是会发现没有从
IntWrapper
到
int
转换来满足左侧的要求,因此丢弃了重写的表达式。 编译器还调用“合成的”表达式
0 < (a <=> 42)
并检测到通过其转换构造函数
IntWrapper
了从
int
到
IntWrapper
的转换,因此使用此候选对象。
合成表达式的目的是避免编写
friend
函数模板来填补可以从其他类型转换对象的空白。 综合表达式一般
0 @ (b <=> a)
。
更复杂的类型
编译器生成的太空飞船运算符不会停留在类的各个成员上;它会为您类型中的所有子对象生成正确的比较集:
struct Basics { int i; char c; float f; double d; auto operator<=>(const Basics&) const = default; }; struct Arrays { int ai[1]; char ac[2]; float af[3]; double ad[2][2]; auto operator<=>(const Arrays&) const = default; }; struct Bases : Basics, Arrays { auto operator<=>(const Bases&) const = default; }; int main() { constexpr Bases a = { { 0, 'c', 1.f, 1. }, { { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } }; constexpr Bases b = { { 0, 'c', 1.f, 1. }, { { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } }; static_assert(a == b); static_assert(!(a != b)); static_assert(!(a < b)); static_assert(a <= b); static_assert(!(a > b)); static_assert(a >= b); }
编译器知道如何将数组的类成员扩展到其子对象列表中,并进行递归比较。 当然,如果您想自己编写这些函数的主体,则仍将受益于编译器重写表达式。
看起来像鸭子,像鸭子一样游泳,像operator==
一样operator==
标准化委员会的一些非常聪明的人已经注意到,无论如何,飞船操作员将始终对元素进行词典编目比较。 字典式比较的无条件执行可能会导致代码效率低下,尤其是使用相等运算符时。
比较两条线的典型示例。 如果您具有字符串
"foobar"
并使用==将该字符串与字符串
"foo"
进行比较,则可以预期该操作几乎是恒定的。 有效的字符串比较算法如下:
- 首先比较两行的大小。 如果大小不同,则返回
false
- 否则,请逐步浏览两行中的每个元素并进行比较,直到存在差异或所有元素结束。 返回结果。
根据飞船操作员的规则,我们必须先比较每个元素,直到找到一个不同的元素。 在我们的示例中,仅当比较
'b'
和
'\0'
时,
"foobar"
和
"foo"
才最终返回
false
。
为了解决这个问题,有篇文章
P1185R2 ,其中详细介绍了编译器如何独立于飞船运算符来重写和生成
operator==
。 我们的
IntWrapper
可以编写如下:
#include <compare> struct IntWrapper { int value; constexpr IntWrapper(int value): value{value} { } auto operator<=>(const IntWrapper&) const = default; bool operator==(const IntWrapper&) const = default; };
又迈出了一步……但是,有个好消息。 您实际上并不需要编写上面的代码,因为只需编写
auto operator<=>(const IntWrapper&) const = default
足以使编译器为您隐式生成一个单独的更有效的
operator==
!
编译器将应用略微修改的“重写”规则,特定于
==
和
!=
,其中在这些运算符中,它们是根据
operator==
而不是
operator<=>
进行重写的。 这意味着
!=
还可以从优化中受益。
旧代码不会中断
此时,您可能会认为:好吧,如果允许编译器执行此操作符重写操作,那么如果我试图超越编译器,将会发生什么:
struct IntWrapper { int value; constexpr IntWrapper(int value): value{value} { } auto operator<=>(const IntWrapper&) const = default; bool operator<(const IntWrapper& rhs) const { return value < rhs.value; } }; constexpr bool is_lt(const IntWrapper& a, const IntWrapper& b) { return a < b; }
答案没什么大不了的。 C ++中的重载解决方案模型是所有候选对象冲突的场所。 在这场特殊的战斗中,我们有三个:
IntWrapper::operator<(const IntWrapper& a, const IntWrapper& b)
IntWrapper::operator<=>(const IntWrapper& a, const IntWrapper& b)
(改写)
IntWrapper::operator<=>(const IntWrapper& b, const IntWrapper& a)
(综合)
如果我们在C ++ 17中采用了重载解析规则,则此调用的结果将是混合的,但C ++ 20的重载解析规则已更改,以便编译器可以将这种情况解决为最合逻辑的重载。
当编译器必须完成一系列额外的遍历时,会有一个过载解决阶段。 在C ++ 20中,出现了一种新机制,其中优先考虑的是不会被覆盖或合成的重载,这使我们的
IntWrapper::operator<
重载了最佳候选并解决了歧义。 相同的机制可以防止使用合成的候选项而不是通常的重写表达式。
最后的想法
太空飞船操作员是C ++的一个受欢迎的补充,因为它可以帮助简化代码并减少编写,有时少则更好。 因此,请系好安全带并控制您的C ++ 20
飞船 !
我们敦促您出去尝试一下太空飞船操作员,它现在在
Visual Studio 2019中的 /std:c++latest
下可用! 请注意,对
P1185R2所做的
更改将在Visual Studio 2019版本16.2中提供。 请记住,宇宙飞船运算符是C ++ 20的一部分,并且可能会有所更改,直到C ++ 20最终定稿为止。
与往常一样,我们等待您的反馈。 随时通过电子邮件将任何评论发送到
visualcpp@microsoft.com ,通过
Twitter @visualc或Facebook
Microsoft Visual Cpp 。
如果您在VS 2019中使用MSVC遇到其他问题,请通过安装程序或Visual Studio IDE本身的
“报告问题”选项通知我们。 有关建议或错误报告,请通过
DevComm给我们
写信。