重载与非布尔返回值的相等比较时,C++20 中的重大变化或 clang-trunk/gcc-trunk 中的回归?

Posted

技术标签:

【中文标题】重载与非布尔返回值的相等比较时,C++20 中的重大变化或 clang-trunk/gcc-trunk 中的回归?【英文标题】:Breaking change in C++20 or regression in clang-trunk/gcc-trunk when overloading equality comparison with non-Boolean return value? 【发布时间】:2020-06-19 10:14:17 【问题描述】:

以下代码在 c++17 模式下使用 clang-trunk 可以正常编译,但在 c++2a(即将推出的 c++20)模式下会中断:

// Meta struct describing the result of a comparison
struct Meta ;

struct Foo 
    Meta operator==(const Foo&) return Meta;
    Meta operator!=(const Foo&) return Meta;
;

int main()

    Meta res = (Foo != Foo);

使用 gcc-trunk 或 clang-9.0.0 也可以正常编译:https://godbolt.org/z/8GGT78

clang-trunk 和-std=c++2a 的错误:

<source>:12:19: error: use of overloaded operator '!=' is ambiguous (with operand types 'Foo' and 'Foo')
    Meta res = (f != g);
                ~ ^  ~
<source>:6:10: note: candidate function
    Meta operator!=(const Foo&) return Meta;
         ^
<source>:5:10: note: candidate function
    Meta operator==(const Foo&) return Meta;
         ^
<source>:5:10: note: candidate function (with reversed parameter order)

我知道 C++20 将使得只重载 operator== 成为可能,编译器将通过否定 operator== 的结果自动生成 operator!=。据我了解,这只适用于返回类型为bool

问题的根源在于,在 Eigen 中,我们在 Array 对象或 Array 和 Scalars 之间声明了一组运算符 ==!=&lt;、...,它们返回(一个表达式的)bool 的数组(然后可以按元素访问,或以其他方式使用)。例如,

#include <Eigen/Core>
int main()

  Eigen::ArrayXd a(10);
  a.setRandom();
  return (a != 0.0).any();

与我上面的示例相比,这甚至因 gcc-trunk:https://godbolt.org/z/RWktKs 而失败。 我还没有设法将其简化为非 Eigen 示例,该示例在 clang-trunk 和 gcc-trunk 中均失败(顶部的示例非常简化)。

相关问题报告:https://gitlab.com/libeigen/eigen/issues/1833

我的实际问题:这实际上是 C++20 中的重大变化(是否有可能重载比较运算符以返回元对象),还是更可能是 clang/gcc 中的回归?

【问题讨论】:

相关:***.com/questions/58319928/… 【参考方案1】:

是的,代码实际上在 C++20 中中断。

表达式Foo != Foo 在 C++20 中有三个候选项(而在 C++17 中只有一个):

Meta operator!=(Foo& /*this*/, const Foo&); // #1
Meta operator==(Foo& /*this*/, const Foo&); // #2
Meta operator==(const Foo&, Foo& /*this*/); // #3 - which is #2 reversed

这来自[over.match.oper]/3.4 中新的重写的候选人 规则。所有这些候选人都是可行的,因为我们的Foo 参数不是const。为了找到最可行的候选人,我们必须通过决胜局。

最佳可行函数的相关规则来自[over.match.best]/2:

鉴于这些定义,如果对于所有参数iICS<sub>i</sub>(F1) 的转换序列不比ICS<sub>i</sub>(F2) 差,并且那么

[...这个例子有很多不相关的案例...],或者,如果不是,那么 F2 是重写的候选者 ([over.match.oper]) 而 F1 不是 F1和F2是重写候选,F2是参数顺序倒序的合成候选,F1不是

#2#3 是重写的候选者,#3 的参数顺序颠倒了,而#1 没有重写。但是为了达到决胜局,我们首先需要通过初始条件:对于所有参数转换序列并不差。

#1#2 好,因为所有的转换序列都是相同的(很简单,因为函数参数相同),#2 是一个重写的候选者,而 #1 不是。

但是...#1/#3#2/#3 都卡在第一个条件上。在这两种情况下,第一个参数对#1/#2 的转换顺序更好,而第二个参数对#3 的转换顺序更好(参数const 必须经过额外的const 限定,因此它的转换顺序更差)。这个const 触发器使我们无法选择其中任何一个。

因此,整个重载解决方案是模棱两可的。

据我了解,这仅适用于返回类型为bool

这是不正确的。我们无条件地考虑重写和颠倒的候选人。我们的规则是,来自[over.match.oper]/9:

如果通过重载决议为运算符@ 选择重写的operator== 候选,则其返回类型应为cv bool

也就是说,我们仍然会考虑这些候选人。但是,如果最佳可行候选者是返回 Metaoperator==,则结果与删除该候选者基本相同。

我们确实希望处于重载决议必须考虑返回类型的状态。无论如何,这里的代码返回Meta这一事实并不重要——如果它返回bool,问题也会存在。


谢天谢地,这里的修复很简单:

struct Foo 
    Meta operator==(const Foo&) const;
    Meta operator!=(const Foo&) const;
    //                         ^^^^^^
;

一旦你将两个比较运算符都设为const,就不会再有歧义了。所有的参数都是一样的,所以所有的转换顺序都是一样的。 #1 现在将通过不重写而击败 #3,并且 #2 现在将通过不被反转来击败 #3 - 这使得 #1 成为最佳可行的候选人。与我们在 C++17 中的结果相同,只需多几步即可实现。

【讨论】:

"我们不希望处于重载解析必须考虑返回类型的状态。" 需要明确的是,重载解析本身并不考虑返回类型,subsequent rewritten operations do。如果重载决议将选择重写的== 并且所选函数的返回类型不是bool,则一个人的代码格式错误。但是这种剔除不会在重载决议本身期间发生。 它实际上只有在返回类型不支持运算符时才格式错误!... @ChrisDodd 不,它必须完全是cv bool(在此更改之前,要求是上下文转换为bool - 仍然不是! 不幸的是,这并不能解决我的实际问题,但那是因为我未能提供一个真正描述我的问题的 MRE。我会接受这一点,当我能够适当地减少我的问题时,我会提出一个新问题...... 看起来对原始问题的适当减少是gcc.godbolt.org/z/tFy4qz【参考方案2】:

Eigen 问题似乎归结为以下几点:

using Scalar = double;

template<class Derived>
struct Base 
    friend inline int operator==(const Scalar&, const Derived&)  return 1; 
    int operator!=(const Scalar&) const;
;

struct X : Base<X> ;

int main() 
    X != 0.0;

表达式的两个候选者是

    来自operator==(const Scalar&amp;, const Derived&amp;)的重写候选 Base&lt;X&gt;::operator!=(const Scalar&amp;) const

根据[over.match.funcs]/4,由于operator!= 没有通过using-declaration 导入X 的范围,#2 的隐式对象参数的类型是const Base&lt;X&gt;&amp;。因此,#1 对该参数具有更好的隐式转换序列(完全匹配,而不是派生到基础的转换)。选择 #1 然后呈现程序格式错误。

可能的修复:

using Base::operator!=; 添加到Derived,或 将operator== 更改为const Base&amp; 而不是const Derived&amp;

【讨论】:

实际代码无法从operator== 返回bool 是否有原因?因为这似乎是新规则下代码格式错误的唯一原因。 实际代码涉及一个operator==(Array, Scalar),它进行元素比较并返回Arraybool。你不能把它变成bool而不破坏其他一切。 这似乎有点像标准中的缺陷。重写 operator== 的规则不应该影响现有代码,但在这种情况下它们会影响,因为检查 bool 返回值不是选择重写候选者的一部分。 @NicolBolas:遵循的一般原则是检查您是否可以做某事(eg,调用操作员),而不是您是否应该,以避免让实现更改默默地影响其他代码的解释。事实证明,重写的比较会破坏很多东西,但主要是已经存在问题且易于修复的东西。所以,无论好坏,这些规则都被采纳了。 哇,非常感谢,我想您的解决方案将解决我们的问题(我目前没有时间以合理的努力安装 gcc/clang 主干,所以我将检查是否会中断直到最新的稳定编译器版本)。【参考方案3】:

[over.match.best]/2 列出了一组有效重载的优先级。 2.8 部分告诉我们 F1F2 更好,如果(在许多其他事情中):

F2 是重写的候选者 ([over.match.oper]) 而F1 不是

那里的示例显示了一个显式的operator&lt; 被调用,即使operator&lt;=&gt; 在那里。

而[over.match.oper]/3.4.3 告诉我们,operator== 在这种情况下的候选资格是重写的候选人。

然而,您的操作员忘记了一件关键的事情:它们应该是const 函数。并且使它们不是const 会导致重载解决方案的早期方面发挥作用。这两个函数都不是完全匹配的,因为不同的参数需要进行非const-to-const 转换。这导致了有问题的模棱两可。

一旦你让他们const,Clang trunk compiles。

我无法与 Eigen 的其余部分交谈,因为我不知道代码,它非常大,因此无法放入 MCVE。

【讨论】:

只有在所有参数的转换都一样好的情况下,我们才会到达您列出的决胜局。但是没有:由于缺少const,未反转的候选者对第二个参数有更好的转换顺序,而反转的候选者对第一个参数有更好的转换顺序。 @RichardSmith:是的,这就是我所说的那种复杂性。但我不想真正经历并阅读/内化这些规则;) 确实,我忘记了最小示例中的const。我很确定 Eigen 在任何地方都使用const(或在类定义之外,也使用const 引用),但我需要检查。当我找到时间时,我尝试将 Eigen 使用的整体机制分解为一个最小的示例。【参考方案4】:

我们的 Goopax 头文件也存在类似问题。使用 clang-10 和 -std=c++2a 编译以下代码会产生编译器错误。

template<typename T> class gpu_type;

using gpu_bool     = gpu_type<bool>;
using gpu_int      = gpu_type<int>;

template<typename T>
class gpu_type

  friend inline gpu_bool operator==(T a, const gpu_type& b);
  friend inline gpu_bool operator!=(T a, const gpu_type& b);
;

int main()

  gpu_int a;
  gpu_bool b = (a == 0);

提供这些额外的运算符似乎可以解决问题:

template<typename T>
class gpu_type

  ...
  friend inline gpu_bool operator==(const gpu_type& b, T a);
  friend inline gpu_bool operator!=(const gpu_type& b, T a);
;

【讨论】:

这不是事先做好的事情吗?否则,how would a == 0 have compiled? 这不是一个真正的类似问题。正如 Nicol 指出的那样,这已经没有在 C++17 中编译。它继续无法在 C++20 中编译,只是出于不同的原因。 我忘了说:我们还提供成员操作符:gpu_bool gpu_type&lt;T&gt;::operator==(T a) const;gpu_bool gpu_type&lt;T&gt;::operator!=(T a) const; 使用 C++-17,这很好用。但是现在有了 clang-10 和 C++-20,这些都找不到了,而是编译器尝试通过交换参数来生成自己的运算符,但它失败了,因为返回类型不是 bool

以上是关于重载与非布尔返回值的相等比较时,C++20 中的重大变化或 clang-trunk/gcc-trunk 中的回归?的主要内容,如果未能解决你的问题,请参考以下文章

Javascript 中 == 和 === 的区别

== 检查布尔值是不是完全相等? - 爪哇

python中的比较运算符

JS中的“==”与强制类型转换

c++中为啥赋值运算符重载返回类型是引用

JS中鲜为人知的问题: [] == ![]结果为true,而 {} == !{}却为false