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

gt0wga4j  于 2023-02-06  发布在  其他
关注(0)|答案(4)|浏览(112)

以下代码在c17模式下使用clang-trunk可以很好地编译,但在c2a(即将推出的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和标量之间声明了一组运算符==!=<......,这些运算符返回bool的Array(的表达式)(然后可以按元素访问,或以其他方式使用)。

#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中的一个回归?

k4emjkb1

k4emjkb11#

是的,代码实际上在C20中中断。
表达式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:
给定这些定义,如果对于所有自变量iICSi(F1)不是比ICSi(F2)更差的转换序列,则可行函数F1被定义为比另一可行函数F2更好的函数,并且

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

#2#3是重写的候选对象,#3的参数顺序相反,而#1没有被重写,但是为了达到那个决胜局,我们需要首先通过初始条件:* 对于所有参数 * 转换序列并不差。
X1 M12 N1 X优于X1 M13 N1 X,因为所有的转换序列都相同(平凡地说,因为函数参数相同),并且X1 M14 N1 X是重写候选,而X1 M15 N1 X不是。
但是......两对#1/#3#2/#3都被第一个条件卡住了。在这两种情况下,第一个参数对于#1/#2具有更好的转换顺序(参数const必须经过额外的const限定,因此它具有更差的转换序列)。该const触发器使我们不能优选任一个。
因此,整个重载解析是不明确的。
据我所知,只有当返回类型为bool时,这才有效。
这是不正确的。我们无条件地考虑重写和反转的候选项。我们的规则是,from [over.match.oper]/9:
如果重载解析为运算符@选择了重写的operator==候选项,则其返回类型应为 cvbool
也就是说,我们仍然考虑这些候选项,但是如果最佳可行候选项是一个operator==,它返回Meta,那么结果基本上与删除该候选项相同。
我们并不希望处于重载解析必须考虑返回类型的状态,而且无论如何,这里的代码返回Meta的事实是无关紧要的--如果它返回bool,这个问题也会存在。
谢天谢地,这里的修复很简单:

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

一旦你使用了两个比较运算符const,就不会有歧义了,所有的参数都是一样的,所以所有的转换序列都是平凡相同的。x1M35 N1 X现在将通过不被重写而击败x1M36 N1 X,并且x1M37 N1 X现在将通过不被反转而击败x1M38 N1 X这使得x1M39 N1 X是最佳可行候选。和我们在C++17中得到的结果一样,只是多了几个步骤。

gfttwv5a

gfttwv5a2#

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

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&)重写候选

  1. Base<X>::operator!=(const Scalar&) const
    根据[over. match. funcs ]/4,由于operator!=不是通过 * using-declaration * 导入到X的作用域中的,因此#2的隐式对象参数的类型为const Base<X>&。因此,#1对该参数具有更好的隐式转换序列(精确匹配,而不是派生到基址的转换)。选择#1会导致程序格式错误。
    可能的修复方法:
  • using Base::operator!=;加到Derived,或
  • operator==更改为采用const Base&而不是const Derived&
mum43rcc

mum43rcc3#

[over.match.best]/2列出了集合中有效重载的优先级。2.8节告诉我们F1优于F2,如果(在 * 许多 * 其他条件中):
F2是重写的候选项([over.match.oper]),而F1不是
这里的示例显示了即使operator<=>在那里,也会调用显式的operator<
而且[over.match.oper]/3.4.3告诉我们,operator==在这种情况下的候选性是一个重写的候选。

  • 然而 *,您的操作员忘记了一件至关重要的事情:它们应该是const函数。使它们不是const会导致重载解决方案的早期方面发挥作用。这两个函数都不是精确匹配的,因为非constconst的转换需要针对不同的参数发生。这导致了问题中的歧义。

一旦你把它们变成constClang trunk compiles
我无法与Eigen的其他部分对话,因为我不知道代码,它非常大,因此无法装入MCVE。

qc6wkl3g

qc6wkl3g4#

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

相关问题