使用非布尔返回值重载相等比较时,C ++ 20中的更改中断还是clang-trunk / gcc-trunk中的回归?


11

以下代码在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

该问题的根源在于,在本征我们声明一组运营商==!=<,...之间Array的对象或Array与标量,其返回(的表达)的阵列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中回归?


Answers:


5

本征问题似乎减少到以下原因:

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. Base<X>::operator!=(const Scalar&) const

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

可能的修复:

  • 添加using Base::operator!=;Derived,或
  • 更改为operator==const Base&代替const Derived&

为何实际代码无法bool从其返回a operator==?因为这似乎是新规则下代码格式错误的唯一原因。
Nicol Bolas

4
实际的代码包含一个operator==(Array, Scalar)进行逐元素比较并返回的Array的代码bool。您不能在bool不破坏其他一切的情况下将其变成一个。
TC

2
这似乎有点像标准中的缺陷。重写规则operator==原本不应该影响现有代码,但在这种情况下它们确实会影响现有代码,因为对bool返回值的检查不是选择要重写的候选对象的一部分。
Nicol Bolas

2
@NicolBolas:遵循的一般原则是,检查是否可以做某事(例如,调用运算符),而不是是否应该这样做,以避免实现更改无提示地影响其他代码的解释。事实证明,重新编写的比较会破坏很多内容,但是大多数情况下,这些内容已经是可疑的并且易于修复。因此,无论好坏,无论如何都采用了这些规则。
戴维斯鲱鱼

哇,非常感谢,我想您的解决方案将解决我们的问题(目前我没有足够的时间来安装gcc / clang干线,因此,我将检查是否可以破坏最新稳定的编译器版本)。
chtz

11

是的,代码实际上在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中重写的候选规则。所有这些候选人都是可行的,因为我们的论据不是。为了找到最合适的人选,我们必须经过决胜局。Fooconst

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

给定这些定义后,如果对于所有参数而言,一个可行的函数F1被定义为比另一个可行的函数更好的函数,而不是比差的转换顺序,然后 F2iICSi(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,而第二参数具有用于更好的转换序列#3(即,参数const必须经历一个额外的const资格,因此它具有较差的转换序列)。这个const触发器使我们无法选择其中一个。

结果,整个过载解决方案是模棱两可的。

据我了解,这仅在return类型为时才有效bool

那是不对的。我们无条件考虑改写和推翻的候选人。我们拥有的规则来自[over.match.oper] / 9

如果operator==通过重载决议为操作员选择了重写的候选项@,则其返回类型应为cv bool

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

我们希望处于过载解析必须考虑返回类型的状态。在任何情况下,这里的代码返回的事实Meta都是无关紧要的-如果返回则该问题也将存在bool


幸运的是,这里的修复很容易:

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

一旦您使两个比较运算符const,就不再有歧义。所有参数都相同,因此所有转换序列都几乎相同。#1现在将#3不被重写而#2击败#3,而现在又将不被击败而击败-这是#1最可行的候选人。我们在C ++ 17中获得了相同的结果,仅需再执行几个步骤即可。


我们不希望处于这样一种状态,其中重载解决方案必须考虑返回类型。 ”需要明确的是,尽管重载解决方案本身并不考虑返回类型,但随后的重写操作却要这样做。如果重载解析会选择重写==,而所选函数的返回类型不是,则代码格式错误bool。但是在过载解析本身期间不会发生这种剔除。
Nicol Bolas

它实际上只有格式错误,如果返回类型不支持运算符!...
克里斯·多德

1
@ChrisDodd不,必须是准确的cv bool(在此更改之前,要求是上下文转换为bool-仍然不是!
Barry

不幸的是,这不能解决我的实际问题,但这是因为我未能提供实际描述我的问题的MRE。我会接受的,当我能够适当地减少问题时,我会问一个新问题……
chtz

2
像原来的问题适当减少长相gcc.godbolt.org/z/tFy4qz
TC

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干线就会编译

我无法与Eigen的其他人交谈,因为我不知道代码,因为代码很大,因此无法放入MCVE中。


2
如果所有参数的转换都一样好,我们只会联系您列出的决胜局。但是没有:由于缺少const,未反转的候选项对第二个参数的转换顺序更好,而反转的候选项对第一个参数的转换顺序更好。
理查德·史密斯

@RichardSmith:是的,这就是我所说的那种复杂性。但我不想真正经历并阅读/内化那些规则;)
Nicol Bolas

确实,我const在最小的示例中忘记了这一点。我可以肯定Eigen const在任何地方(或类定义之外,还有const引用)都可以使用,但是我需要检查一下。当我发现时间时,我尝试将Eigen使用的总体机制分解为一个最小的示例。
chtz

-1

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

1
难道这不是事前有用的事情吗?否则,将如何a == 0编译
Nicol Bolas

这实际上不是一个类似的问题。正如Nicol所指出的那样,这尚未在C ++ 17中进行编译。只是出于另一个原因,它仍然无法在C ++ 20中进行编译。
巴里

我忘了提:我们还提供成员运算符: gpu_bool gpu_type<T>::operator==(T a) const;并且gpu_bool gpu_type<T>::operator!=(T a) const;对于C ++-17,这很好用。但是,现在使用clang-10和C ++-20时,将不再找到它们,而是编译器尝试通过交换参数来生成自己的运算符,但由于返回类型为not,它会失败bool
Ingo Josopait
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.