用火箭科学简化你的代码:C++ 20飞船操作员

这篇文章是 常规系列职位 这里的C++产品团队在微软和其他客人回答我们收到的客户提问。这些问题可以是任何C++相关的:MSVC工具集,标准语言和库,C++标准委员会,ISOCPP.ORG,CppCon等等。今天的帖子是Cameron DaCamara。

null

C++ 20添加了一个新的操作符,亲切地称为“飞船”操作员: <=> . 有一个 邮递 一段时间后回到我们自己的身边 西蒙·布兰德 详细介绍有关这个新操作符的一些信息,以及有关它是什么和做什么的一些概念性信息。  这篇文章的目的是探索这个奇怪的新操作符及其相关的对应操作符的一些具体应用 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 helper但是注意到所有的比较运算符都没有异常规范,并且经历了相同的繁琐的添加过程 noexcept 5个过载中的每一个。

这是C++ 20新飞船操作员介入帮助我们的地方。让我们看看原版 IntWrapper 可以在C++ 20世界中编写:

#include <compare>
struct IntWrapper {
  int value;
  constexpr IntWrapper(int value): value{value} { }
  auto operator<=>(const IntWrapper&) const = default;
};

您可能会注意到的第一个区别是 <compare> . 这个 <compare> header负责用spaceship操作符返回适合我们的默认函数的类型所需的所有比较类别类型填充编译器。在上面的代码段中,返回类型 auto 将被推断为 std::strong_ordering .

我们不仅删除了5行多余的代码,而且我们甚至不需要定义任何东西,编译器为我们做了!我们的 is_lt 保持不变,只是工作,而仍然是 constexpr 尽管我们没有在默认的 operator<=> . 这很好,但有些人可能会抓挠他们的头 为什么? is_lt 即使它根本不使用太空船操作符,仍然可以编译。让我们来探索这个问题的答案。

重写表达式

在C++ 20中,编译器被引入到一个新的概念中,称为“重写”表达式。宇宙飞船操作员,以及 operator== ,是前两位需要重写表达的候选人之一。对于表达式重写的更具体的示例,让我们分解中提供的示例 is_lt .

在重载解析过程中,编译器将从一组可行的候选者中进行选择,所有候选者都匹配我们要查找的运算符。对于关系和等价操作的情况,候选收集过程变化很小,其中编译器还必须收集特殊的重写和合成候选( [结束匹配操作]/3.4 ).

为了我们的表达 a < b 标准规定我们可以搜索 a 对于一个 operator<=> 或命名空间作用域函数 operator<=> 接受它的类型。所以编译器发现, a 的类型不包含 IntWrapper::operator<=> . 然后允许编译器使用该运算符并重写表达式 a < b 作为 (a <=> b) < 0 . 然后将重写的表达式用作正常重载解析的候选表达式。

你可能会问为什么这个重写的表达式是有效的和正确的。表达式的正确性实际上源于spaceship操作符提供的语义。这个 <=> 是一个三方比较,它意味着你得到的不仅仅是一个二进制结果,还有一个排序(在大多数情况下),如果你有一个排序,你可以用任何关系操作来表达这个排序。一个简单的例子,表达式 4 <=> 5 C++ 20会把结果还给你 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的土地上是有意义的,解决这个问题的方法是增加一些额外的东西。 friend 函数到 IntWrapper 它的左边 int . 如果你试图用C++ 20编译器和C++ 20的定义来创建那个示例 IntWrapper 你可能会注意到,它,再次,“只是工作”-另一个挠头。让我们来看看为什么上面的代码仍然允许在C++ 20中编译。

在重载解析期间,编译器还将收集标准所指的“合成”候选对象,或参数顺序颠倒的重写表达式。在上面的示例中,编译器将尝试使用重写的表达式 (42 <=> a) < 0 但它会发现没有从 IntWrapper int 满足左边的条件,以便删除重写的表达式。编译器还产生“合成”表达式 0 < (a <=> 42) 发现有一个 int IntWrapper 通过它的转换构造函数来使用这个候选者。

合成表达式的目标是避免需要编写 friend 函数来填补可以从其他类型转换对象的空白。将合成表达式推广到 0 @ (b <=> a) .

更复杂的类型

编译器生成的spaceship操作符不会停留在类的单个成员上,它将为类型中的所有子对象生成一组正确的比较:

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==

标准化委员会的一些非常聪明的人注意到,不管发生什么,宇宙飞船操作员总是会对元素进行词典比较。无条件地执行字典比较会导致生成效率低下的代码,尤其是使用相等运算符。

典型的例子是比较两个字符串。如果你有绳子 "foobar" 你把它和字符串比较一下 "foo" 使用 == 人们会期望这种操作几乎是不变的。因此,有效的字符串比较算法是:

  • 首先比较两个字符串的大小,如果大小不同则返回 false ,否则
  • 一步一步地遍历两个字符串中的每个元素,并进行比较,直到其中一个不同或到达末尾,然后返回结果。

根据宇宙飞船操作规则,我们需要 开始 先对每个元素进行深入的比较,直到找到不同的元素。在我们的例子中 "foobar" "foo" 仅在比较时 'b' ' ' 你终于回来了吗 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过载解决规则被改变,以便编译器能够将这种情况解析为最逻辑的过载。

在重载解析的一个阶段,编译器必须执行一系列的tiebrukers。在C++ 20中,有一个新的Te断路器,它表示我们必须重写未重写或合成的重载,这使得我们的过载。 IntWrapper::operator< 最好的候选人和解决歧义。同样的机制可以防止合成的候选对象在正则重写表达式上跺脚。

结束语

飞船操作员是C++的欢迎添加,它是简化和帮助你编写的特性之一。 较少的 代码,有时,少就是多。所以用C++ 20的 宇宙飞船 接线员!

我们劝你出去试试宇宙飞船操作员,现在在伦敦有 Visual Studio 2019 在下面 /std:c++latest ! 值得注意的是,通过 P1185R2页 将在Visual Studio 2019版本16.2中提供。请记住飞船操作员是C++ 20的一部分,并且在某些时候发生变化,直到C++ 20完成。

一如既往,我们欢迎您的反馈。欢迎通过电子邮件发送任何评论 visualcpp@microsoft.com ,通过 推特@visualc ,或Facebook Microsoft Visual Cpp . 另外,请随时在Twitter上关注我 @星际克隆 .

如果您在VS 2019中遇到MSVC的其他问题,请通过 报告问题 选项,从安装程序或VisualStudioIDE本身。如需建议或错误报告,请通过 开发命令。

© 版权声明
THE END
喜欢就支持一下吧,技术咨询可以联系QQ407933975
点赞0 分享