当与非布尔 return 值重载相等比较时,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?

以下代码在 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!=。据我了解,这仅在 return 类型为 bool.

时有效

问题的根源在于,在 Eigen 中我们声明了一组运算符 ==!=<、... Array 对象或 Array 和标量,其中 return(表达式)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 中的一个重大变化(并且是否有可能将比较运算符重载到 return 元对象),或者它更可能是回归clang/gcc?

[over.match.best]/2 列出了集合中有效重载的优先级。 2.8 部分告诉我们 F1 优于 F2 如果(在 许多 其他事物中):

F2 is a rewritten candidate ([over.match.oper]) and F1 is not

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

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

但是,您的操作员忘记了一件重要的事情:它们应该是const 函数。并使它们不 const 导致重载决议的早期方面发挥作用。这两个函数都不是完全匹配的,因为不同的参数需要进行非 constconst 的转换。这导致了所讨论的歧义。

制作完成后 constClang trunk compiles.

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

是的,代码实际上在 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:

Given these definitions, a viable function F1 is defined to be a better function than another viable function F2 if for all arguments i, ICS<sub>i</sub>(F1) is not a worse conversion sequence than ICS<sub>i</sub>(F2), and then

  • [... lots of irrelevant cases for this example ...] or, if not that, then
  • F2 is a rewritten candidate ([over.match.oper]) and F1 is not
  • F1 and F2 are rewritten candidates, and F2 is a synthesized candidate with reversed order of parameters and F1 is not

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

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

但是... #1/#3#2/#3 都卡在第一个条件上。在这两种情况下,第一个参数对于 #1/#2 具有更好的转换顺序,而第二个参数对于 #3 具有更好的转换顺序(const 的参数具有接受额外的 const 资格,因此它的转换顺序更差)。这个 const 触发器导致我们无法偏爱任何一个。

因此,整个重载决议是不明确的。

As far as I understand, this only works as long as the return type is bool.

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

If a rewritten operator== candidate is selected by overload resolution for an operator @, its return type shall be cv bool

也就是我们还是考虑这些人选。但是,如果最可行的候选者是 return 的 operator==,比如说 Meta - 结果基本上与删除该候选者相同。

我们不想处于重载解析必须考虑return类型的状态。无论如何,这里的代码 returns Meta 是无关紧要的 - 如果它 returned bool.

也会存在问题

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

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

一旦你使两个比较运算符成为const,就没有更多的歧义了。所有参数都相同,因此所有转换序列基本相同。 #1 现在可以通过不重写打败 #3 并且 #2 现在可以通过不被逆转打败 #3 - 这使得 #1 成为最可行的候选者。与我们在 C++17 中得到的结果相同,只是多了几个步骤。

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

表达式的两个候选是

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

Per [over.match.funcs]/4,因为 operator!= 没有通过 using-declaration 导入到 X 的范围内, #2 的隐式对象参数是 const Base<X>&。因此,#1 对该参数有更好的隐式转换序列(精确匹配,而不是派生到基础的转换)。选择 #1 会导致程序格式错误。

可能的修复:

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

我们的 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);
};