重载与非布尔返回值的相等比较时,C++20 中的重大变化或 clang-trunk/gcc-trunk 中的回归?
Posted
技术标签:
【中文标题】重载与非布尔返回值的相等比较时,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? 【发布时间】:2020-06-19 10:14:17 【问题描述】:以下代码在 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!=
。据我了解,这只适用于返回类型为bool
。
问题的根源在于,在 Eigen 中,我们在 Array
对象或 Array
和 Scalars 之间声明了一组运算符 ==
、!=
、<
、...,它们返回(一个表达式的)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 中的重大变化(是否有可能重载比较运算符以返回元对象),还是更可能是 clang/gcc 中的回归?
【问题讨论】:
相关:***.com/questions/58319928/… 【参考方案1】:是的,代码实际上在 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:
鉴于这些定义,如果对于所有参数
[...这个例子有很多不相关的案例...],或者,如果不是,那么 F2 是重写的候选者 ([over.match.oper]) 而 F1 不是 F1和F2是重写候选,F2是参数顺序倒序的合成候选,F1不是i
、ICS<sub>i</sub>(F1)
的转换序列不比ICS<sub>i</sub>(F2)
差,并且那么
#2
和#3
是重写的候选者,#3
的参数顺序颠倒了,而#1
没有重写。但是为了达到决胜局,我们首先需要通过初始条件:对于所有参数转换序列并不差。
#1
比 #2
好,因为所有的转换序列都是相同的(很简单,因为函数参数相同),#2
是一个重写的候选者,而 #1
不是。
但是...#1
/#3
和 #2
/#3
都卡在第一个条件上。在这两种情况下,第一个参数对#1
/#2
的转换顺序更好,而第二个参数对#3
的转换顺序更好(参数const
必须经过额外的const
限定,因此它的转换顺序更差)。这个const
触发器使我们无法选择其中任何一个。
因此,整个重载解决方案是模棱两可的。
据我了解,这仅适用于返回类型为
bool
。
这是不正确的。我们无条件地考虑重写和颠倒的候选人。我们的规则是,来自[over.match.oper]/9:
如果通过重载决议为运算符
@
选择重写的operator==
候选,则其返回类型应为cvbool
也就是说,我们仍然会考虑这些候选人。但是,如果最佳可行候选者是返回 Meta
的 operator==
,则结果与删除该候选者基本相同。
我们确实不希望处于重载决议必须考虑返回类型的状态。无论如何,这里的代码返回Meta
这一事实并不重要——如果它返回bool
,问题也会存在。
谢天谢地,这里的修复很简单:
struct Foo
Meta operator==(const Foo&) const;
Meta operator!=(const Foo&) const;
// ^^^^^^
;
一旦你将两个比较运算符都设为const
,就不会再有歧义了。所有的参数都是一样的,所以所有的转换顺序都是一样的。 #1
现在将通过不重写而击败 #3
,并且 #2
现在将通过不被反转来击败 #3
- 这使得 #1
成为最佳可行的候选人。与我们在 C++17 中的结果相同,只需多几步即可实现。
【讨论】:
"我们不希望处于重载解析必须考虑返回类型的状态。" 需要明确的是,重载解析本身并不考虑返回类型,subsequent rewritten operations do。如果重载决议将选择重写的==
并且所选函数的返回类型不是bool
,则一个人的代码格式错误。但是这种剔除不会在重载决议本身期间发生。
它实际上只有在返回类型不支持运算符时才格式错误!...
@ChrisDodd 不,它必须完全是cv bool
(在此更改之前,要求是上下文转换为bool
- 仍然不是!
)
不幸的是,这并不能解决我的实际问题,但那是因为我未能提供一个真正描述我的问题的 MRE。我会接受这一点,当我能够适当地减少我的问题时,我会提出一个新问题......
看起来对原始问题的适当减少是gcc.godbolt.org/z/tFy4qz【参考方案2】:
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;
表达式的两个候选者是
-
来自
operator==(const Scalar&, const Derived&)
的重写候选
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&
。
【讨论】:
实际代码无法从operator==
返回bool
是否有原因?因为这似乎是新规则下代码格式错误的唯一原因。
实际代码涉及一个operator==(Array, Scalar)
,它进行元素比较并返回Array
的bool
。你不能把它变成bool
而不破坏其他一切。
这似乎有点像标准中的缺陷。重写 operator==
的规则不应该影响现有代码,但在这种情况下它们会影响,因为检查 bool
返回值不是选择重写候选者的一部分。
@NicolBolas:遵循的一般原则是检查您是否可以做某事(eg,调用操作员),而不是您是否应该,以避免让实现更改默默地影响其他代码的解释。事实证明,重写的比较会破坏很多东西,但主要是已经存在问题且易于修复的东西。所以,无论好坏,这些规则都被采纳了。
哇,非常感谢,我想您的解决方案将解决我们的问题(我目前没有时间以合理的努力安装 gcc/clang 主干,所以我将检查是否会中断直到最新的稳定编译器版本)。【参考方案3】:
[over.match.best]/2 列出了一组有效重载的优先级。 2.8 部分告诉我们 F1
比 F2
更好,如果(在许多其他事情中):
F2
是重写的候选者 ([over.match.oper]) 而F1
不是
那里的示例显示了一个显式的operator<
被调用,即使operator<=>
在那里。
而[over.match.oper]/3.4.3 告诉我们,operator==
在这种情况下的候选资格是重写的候选人。
然而,您的操作员忘记了一件关键的事情:它们应该是const
函数。并且使它们不是const
会导致重载解决方案的早期方面发挥作用。这两个函数都不是完全匹配的,因为不同的参数需要进行非const
-to-const
转换。这导致了有问题的模棱两可。
一旦你让他们const
,Clang trunk compiles。
我无法与 Eigen 的其余部分交谈,因为我不知道代码,它非常大,因此无法放入 MCVE。
【讨论】:
只有在所有参数的转换都一样好的情况下,我们才会到达您列出的决胜局。但是没有:由于缺少const
,未反转的候选者对第二个参数有更好的转换顺序,而反转的候选者对第一个参数有更好的转换顺序。
@RichardSmith:是的,这就是我所说的那种复杂性。但我不想真正经历并阅读/内化这些规则;)
确实,我忘记了最小示例中的const
。我很确定 Eigen 在任何地方都使用const
(或在类定义之外,也使用const
引用),但我需要检查。当我找到时间时,我尝试将 Eigen 使用的整体机制分解为一个最小的示例。【参考方案4】:
我们的 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);
;
【讨论】:
这不是事先做好的事情吗?否则,how woulda == 0
have compiled?
这不是一个真正的类似问题。正如 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,这些都找不到了,而是编译器尝试通过交换参数来生成自己的运算符,但它失败了,因为返回类型不是 bool
。以上是关于重载与非布尔返回值的相等比较时,C++20 中的重大变化或 clang-trunk/gcc-trunk 中的回归?的主要内容,如果未能解决你的问题,请参考以下文章