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

cht*_*htz 14 c++ language-lawyer eigen eigen3 c++20

以下代码在 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{});
}
Run Code Online (Sandbox Code Playgroud)

使用 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)
Run Code Online (Sandbox Code Playgroud)

我知道 C++20 将使仅重载成为可能,operator==编译器将operator!=通过否定operator==. 据我了解,这仅在返回类型为bool.

该问题的根源在于,在本征我们声明一组运营商==!=<,...之间Array的对象或Array与标量,其返回(的表达)的阵列bool(其然后可被访问逐元素,或以其他方式使用)。例如,

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

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

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

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

Bar*_*rry 13

是的,代码实际上在 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
Run Code Online (Sandbox Code Playgroud)

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

最佳可行功能的相关规则来自[over.match.best]/2

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

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

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

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

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

因此,整个重载决议是模棱两可的。

据我了解,这仅在返回类型为bool.

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

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

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

我们并没有希望的状态下超负荷决议将不得不考虑返回类型。无论如何,这里的代码返回的事实Meta是无关紧要的——如果它返回bool.


值得庆幸的是,这里的修复很简单:

struct Foo {
    Meta operator==(const Foo&) const;
    Meta operator!=(const Foo&) const;
    //                         ^^^^^^
};
Run Code Online (Sandbox Code Playgroud)

一旦你同时使用了两个比较运算符const,就不会有歧义了。所有参数都相同,因此所有转换序列几乎相同。#1现在将#3通过不被重写而#2击败#3,现在将通过不被逆转而击败——这使得#1最好的可行候选者。与我们在 C++17 中得到的结果相同,只是多走几步即可到达那里。

  • 看起来原始问题的适当减少是 https://gcc.godbolt.org/z/tFy4qz (2认同)

T.C*_*.C. 6

本征问题似乎简化为以下几点:

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;
}
Run Code Online (Sandbox Code Playgroud)

表达式的两个候选是

  1. 改写的候选人来自 operator==(const Scalar&, const Derived&)
  2. Base<X>::operator!=(const Scalar&) const

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

可能的修复:

  • 添加using Base::operator!=;Derived,或
  • 将 更改operator==为使用 aconst Base&而不是 a const Derived&

  • 实际代码涉及一个“operator==(Array, Scalar)”,它执行逐元素比较并返回“bool”的“Array”。你不能把它变成“bool”而不破坏其他一切。 (4认同)
  • 这看起来有点像标准中的缺陷。重写“operator==”的规则不应该影响现有代码,但在这种情况下却会影响现有代码,因为对“bool”返回值的检查不是选择重写候选者的一部分。 (2认同)
  • @NicolBolas:遵循的一般原则是检查您是否*可以*做某事(*例如*,调用运算符),而不是您是否*应该*,以避免实现更改默默地影响其他代码的解释。事实证明,重写的比较破坏了很多东西,但大多数东西已经有问题并且很容易修复。因此,无论好坏,这些规则无论如何都被采用了。 (2认同)

Nic*_*las 5

[over.match.best]/2 列出了集合中有效重载的优先级。第2.8节告诉我们这F1F2if更好(以及许多其他方面):

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

那里的示例显示了一个显式operator<被调用,即使operator<=>存在。

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

但是,您的操作员忘记了一件至关重要的事情:它们应该是const函数。并使它们不会const导致重载决议的早期方面发挥作用。无论是功能是完全匹配的,因为不const至-const转换需要发生了不同的参数。这导致了所讨论的歧义。

一旦你创建了它们constClang trunk 就会编译

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

  • 只有当所有参数都有同样好的转换时,我们才会进入您列出的决胜局。但事实并非如此:由于缺少“const”,非反转候选者对第二个参数有更好的转换序列,而反转候选者对第一个参数有更好的转换序列。 (2认同)