用火箭科学简化您的代码: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 ,drat! 因此,有人将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条多余的行,而且甚至不必定义任何内容,编译器为我们做到了! 即使未在默认的operator<=>明确指定,我们的is_lt仍保持不变,并且仍可以在constexpr 。 很好,但是有些人可能会scratch之以is_lt ,尽管为什么is_lt甚至根本不使用太空飞船运算符,但为什么is_lt允许编译is_lt 。 让我们探索这个问题的答案。

重写表达式


在C ++ 20中,编译器被引入了一个称为“重写”表达式的新概念。 宇宙飞船算子以及operator==属于前两个要重写表达式的候选对象。 有关表达式重写的更具体示例,让我们分解is_lt提供的is_lt

在重载解析期间,编译器将从一组可行的候选对象中进行选择,所有这些候选对象均与我们正在寻找的运算符匹配。 对于关系和等效操作,候选者的收集过程稍有变化,在这种情况下,编译器还必须收集经过重写和合成的特殊候选者( [over.match.oper] /3.4 )。

对于我们的表达式a < b ,标准声明我们可以在a的类型中搜索一个operator<=>或一个接受其类型的名称空间作用域operator<=> 。 因此,编译器这样做,并且发现实际上,的类型确实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添加一些额外的friend函数,这些函数采用int的左侧。 如果您尝试使用C ++ 20编译器和我们的IntWrapper C ++ 20定义来构建该示例,您可能会注意到它再次“起作用”,这是另一种方法。 让我们研究一下为什么上面的代码仍允许在C ++ 20中进行编译。

在重载解析期间,编译器还将收集标准所指的“综合”候选项,或以参数顺序相反的方式重写表达式。 在上面的示例中,编译器将尝试使用重写的表达式(42 <=> a) < 0但会发现从IntWrapperint没有满足左侧转换的要求,因此删除了重写的表达式。 编译器还构想出“合成的”表达式0 < (a <=> 42)并发现通过其转换构造函数从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 ,否则
  • 逐步遍历两个字符串的每个元素,并进行比较直到一个不同或到达末尾,然后返回结果。

根据太空飞船操作员规则,我们需要首先对每个元素进行深度比较,直到找到一个不同的元素。 在我们的"foobar""foo"示例中,只有将'b''\0'进行比较时,您最终才返回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 ++中的重载解决方案模型具有所有候选者都在其中进行战斗的领域,在这个特定的战斗中,我们有3个候选者:

  • 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.comTwitter @visualcMicrosoft Visual Cpp的 Facebook发送任何评论。 另外,请随时在Twitter @starfreakclone上关注我。

如果您在VS 2019中使用MSVC遇到其他问题,请通过安装程序或Visual Studio IDE本身的报告问题选项通知我们。 有关建议或错误报告,请通过DevComm告诉我们

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


All Articles