提问者:小点点

当重载与非布尔返回值的相等比较时,C 20中的中断更改或clang-主干/gcc-主干中的回归?


以下代码在 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-0012或clang-9.0.0:https://godbolt.org/z/8GGT78

clang-trunk和< code>-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将使只重载< code>operator==成为可能,编译器将自动生成< code>operator!=通过否定< code >运算符==的结果。据我理解,这只在返回类型为< code>bool时才有效。

问题的根源是在本征中我们声明一组运算符 ==, !=

#include <Eigen/Core>
int main()
{
    Eigen::ArrayXd a(10);
    a.setRandom();
    return (a != 0.0).any();
}

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

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

我的实际问题是:这实际上是C 20中的一个突破性变化吗(是否有可能重载比较操作符以返回元对象),还是更有可能是clang/gcc中的一个回归?


共3个答案

匿名用户

是的,代码实际上在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中新改写的候选规则。所有这些候选都是可行的,因为我们的< code>Foo参数不是< code>const。为了找到最佳候选人,我们必须通过决胜局。

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

给定这些定义,如果对于所有参数 i,ICS i(F1) 不是比 ICSiF2) 更差的转换序列,则可行函数 F1 被定义为比另一个可行函数 F2 更好的函数,然后

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

#2#3是重写的候选者,#3颠倒了参数顺序,而#1没有重写。但是为了打破平局,我们需要首先通过初始条件:对于所有参数,转换序列都不会更糟。

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

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

因此,整个重载分辨率不明确。

据我所知,只有当返回类型为<code>bool</code>时,这才有效。

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

如果通过重载解析为运算符@选择重写的运算符==候选者,则其返回类型应为cvbool

就是我们还是考虑这些候选人。但是,如果最佳候选项是返回< code>Meta的< code>operator==,那么结果基本上与候选项被删除的情况相同。

我们不希望处于重载解析必须考虑返回类型的状态。在任何情况下,这里的代码返回Meta这一事实都无关紧要——如果它返回bool,问题也会存在。

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

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

一旦您使两个比较操作符< code>const,就不再有歧义了。所有的参数都是一样的,所以所有的转换序列都是一样的。< code>#1现在将通过不重写来击败< code>#3,而< code>#2现在将通过不反转来击败< code>#3,这使得< code>#1成为最佳可行候选。和C 17的结果一样,只是多了几步。

匿名用户

特征问题似乎归结为以下内容:

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;
}

表达式的两个候选者是

  1. 来自运算符==(const标量

Per [over.match.funcs]/4,as 运算符!=没有通过using声明导入到< code>X的范围中,则#2的隐式对象参数的类型是< code>const Base

可能的修复:

  • 使用Base::运算符!=;添加<code>到派生,或
  • 更改运算符==以获取常量Base

匿名用户

[over.match.best]/2列出了如何对集合中的有效重载进行优先级排序。第2.8节告诉,如果(在许多其他方面):

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

该示例显示了一个显式的< code >运算符

[over.match.oper]/3.4.3告诉我们,在这种情况下,操作符==的候选项是重写的候选项。

但是,您的运算符忘记了一件关键的事情:它们应该是常量函数。并且使它们不常量会导致过载分辨率的早期方面发挥作用。这两个函数都不是完全匹配的,因为对于不同的参数,需要发生非常常量的转换。这导致了所讨论的模棱两可。

一旦你使它们变得恒定,Clang trunk 就会编译。

我不能和Eigen的其他人说话,因为我不知道代码,它太大了,因此不适合MCVE。