【问题标题】:What are the breaking changes caused by rewritten comparison operators?重写比较运算符会带来哪些重大变化?
【发布时间】:2021-01-15 16:43:14
【问题描述】:

在 C++20 中有一些关于重写比较运算符的新规则,我试图了解它们是如何工作的。我遇到了以下program

struct B {};

struct A
{
    bool operator==(B const&);  // #1
};

bool operator==(B const&, A const&);  // #2

int main()
{
  B{} == A{};  // C++17: calls #2
               // C++20: calls #1
}

这实际上破坏了现有代码。我对此感到有些惊讶; #2 实际上对我来说还是更好看:p

那么这些新规则如何改变现有代码的含义?

【问题讨论】:

  • 哪个编译器?其他编译器做同样的事情吗?如果您删除两种语言版本的任一定义,会发生什么情况?
  • @1201ProgramAlarm gcc 和 clang 据我所知具有一致的行为。在两个版本中删除 #1 调用 #2 并删除 #2 无法在 c++17 中编译,这两者都不是重大更改。我认为这两个结果都是预期的。
  • 这似乎是因为 [over.match.oper] 中的内容(第 12.4.1.2 节 N4849 中的表达式中的运算符)。特别是关于 rewritten Candidates 的段落都是新的,并且其中的措辞似乎允许使用y==x(反转操作数)。我不确定答案中应该包含多少。
  • @1201ProgramAlarm 你说得对,我实际上并不想要具体的规则本身,我可以解决这个问题。更我不明白 为什么 #1 是更好的选择。我不知道如何改写这个问题。
  • @1201ProgramAlarm 好的,稍微编辑一下问题。现在只需要解释为什么程序的行为发生变化所需的规则,需要在答案中。

标签: c++ language-lawyer c++20 comparison-operators


【解决方案1】:

该特定方面是一种简单的重写形式,即反转操作数。主运算符==<=> 可以颠倒,辅助运算符!=<><=>= 可以根据主运算符重写。

倒车方面可以用一个比较简单的例子来说明。

如果您没有特定的B::operator==(A) 来处理b == a,则可以使用相反的方法来代替:A::operator==(B)。这是有道理的,因为相等是一种双向关系:(a == b) => (b == a)

另一方面,重写辅助运算符涉及使用不同的运算符。考虑a > b。如果您无法找到直接执行此操作的函数,例如 A::operator>(B),则该语言将查找 A::operator<=>(B) 之类的内容,然后简单地从中计算结果。

这是一个简单的过程视图,但我的大多数学生似乎都理解它。如果您想了解更多详细信息,请参阅 C++20 的 [over.match.oper] 部分,这是重载决议的一部分(@ 是运算符的占位符):

对于关系和相等运算符,重写的候选包括运算符 <=> 的所有成员、非成员和内置候选,其中重写的表达式 (x <=> y) @ 0 使用 operator<=> 格式正确。

对于关系、相等和三路比较运算符,重写的候选还包括一个合成候选,两个参数的顺序颠倒,对于每个成员、非成员和内置候选 运算符<=>,其重写表达式0 @ (y <=> x) 是使用operator<=> 格式良好的。


因此,必须提供真实的 operator==operator<,然后是样板化的日子已经一去不复返了:

operator!=      as      !  operator==
operator>       as      ! (operator== || operator<)
operator<=      as         operator== || operator<
operator>=      as      !  operator<

如果我有一个或多个错误,请不要抱怨,这只是说明了我的观点,即 C++20 有多好,因为您现在只需要提供一个最小集合(很可能只是 @987654349 @加上你想要的任何其他效率)并让编译器照顾它:-)


这个代码可以看出为什么选择一个而不是另一个的问题:

#include <iostream>

struct B {};
struct A {
    bool operator==(B const&) { std::cout << "1\n"; return true; }
};
bool operator==(B const&, A const&) { std::cout << "2\n"; return true; }

int main() {
  auto b = B{}; auto a = A{};

           b ==          a;  // outputs: 1
  (const B)b ==          a;  //          1
           b == (const A)a;  //          2
  (const B)b == (const A)a;  //          2
}

它的输出表明这是aconst-ness 决定哪个是更好的候选者。

顺便说一句,您可能想看看this article,它提供了更深入的了解。

【讨论】:

  • 感谢您的回答,但问题是为什么合成的候选者 (#1) 更匹配?
  • 参数不是常量。该成员是非常量的,所以它获胜。
  • @T.C.啊,我想就是这样。为了澄清,重写的运算符模仿了原始运算符的 const 限定符?
  • RE:因此,必须提供真正的 operator== 和 operator的日子已经一去不复返了:这就是我当初所要求的问this 的问题,就剩下的实现一个运算符!...还是我又错了?
【解决方案2】:

从非语言律师的意义上说,它是这样工作的。 C++20 要求operator== 计算两个对象是否相等。相等的概念是可交换的:如果 A == B,则 B == A。因此,如果有两个 operator== 函数可以被 C++20 的参数反转规则调用,那么你的代码应该表现 同样无论哪种方式。

基本上,C++20 的意思是,如果调用哪一个很重要,那么您就错误地定义了“平等”。


那么让我们进入细节。我所说的“细节”是指标准中最可怕的一章:函数重载解析。

[over.match.oper]/3 定义了构建运算符重载的候选函数集的机制。 C++20 通过引入"rewritten candidates" 对此进行了补充:通过以 C++20 认为在逻辑上等效的方式重写表达式而发现的一组候选函数。这仅适用于关系和 in/equality 运算符。

该集合是按照以下内容构建的:

  • 对于关系 ([expr.rel]) 运算符,重写的候选包括表达式 x y 的所有未重写的候选。
  • 对于关系 ([expr.rel]) 和三向比较 ([expr.spaceship]) 运算符,重写的候选还包括一个合成候选,两个参数的顺序颠倒,对于每个未重写表达式 y x.
  • 的候选者
  • 对于 != 运算符 ([expr.eq]),重写的候选包括表达式 x == y 的所有未重写的候选。
  • 对于等式运算符,重写的候选还包括一个合成候选,两个参数的顺序颠倒,用于表达式 y == x 的每个未重写候选。
  • 对于所有其他运算符,重写的候选集为空。

请注意“综合候选者”的特定概念。这是“反转论点”的标准说法。

本节的其余部分详细说明了如果选择了重写的候选者之一意味着什么(又名:如何综合调用)。为了找出哪个候选人被选中,我们必须深入研究 C++ 标准中最恐怖的章节中最恐怖的部分:

Best viable function matching.

这里重要的是这个声明:

一个可行函数F1被定义为比另一个可行函数F2更好的函数,如果对于所有参数i,ICSi(F1)不是比ICSi(F2)更差的转换序列,那么

这很重要...因为this。从字面上看。

根据[over.ics.scs] 的规则,身份转换比添加限定符的转换更好。

A{} 是纯右值,而且...不是const。成员函数的this 参数也不是。所以这是一个恒等转换,这是一个比非成员函数的const A&amp; 更好的转换序列。

是的,下面有一条规则明确地使候选列表中的重写函数不太可行。不过没关系,因为重写后的调用更好地匹配函数参数单独

如果您使用显式变量并像A const a{}; 这样声明一个变量,那么[over.match.best]/2.8 会参与其中并取消重写版本的优先级。 As seen here. 同样,如果你把成员函数设为const,你也可以get consistent behavior

【讨论】:

  • 这很有意义。是否在任何地方明确说明重写的运算符与原始运算符具有相同的 const 限定?还是那不重要?
  • @cigien:我不确定你的意思。 A{} == B{}const 资格的影响得到了很好的说明。因此,当标准要求根据表达式 y == x 查找候选人时,无需多说。
  • 我可能误解了这一点;编译器看到A::operator==(B const&amp;) 并合成类似B::operator==(A const&amp;) 的东西。那正确吗?如果是这样,编译器是否可以合成B::operator==(A const&amp;) const,这对于这个运算符来说更自然?
  • @cigien:编译器不合成函数。它合成表达式,然后解析为重载解析期间考虑的重载函数集。只需看看我从标准中引用的文本,了解它是如何工作的。
  • bool A::operator==(const B&amp;) constis selected
猜你喜欢
  • 2018-11-21
  • 1970-01-01
  • 1970-01-01
  • 2016-11-12
  • 1970-01-01
  • 1970-01-01
  • 2011-08-12
  • 2011-03-10
  • 2012-01-26
相关资源
最近更新 更多