C ++ 20中的新太空飞船运算符

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::lessstd::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 ,但是会发现没有从IntWrapperint转换来满足左侧的要求,因此丢弃了重写的表达式。 编译器还调用“合成的”表达式0 < (a <=> 42)并检测到通过其转换构造函数IntWrapper了从intIntWrapper的转换,因此使用此候选对象。

合成表达式的目的是避免编写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给我们写信。

Source: https://habr.com/ru/post/zh-CN458242/


All Articles