Clang和GCC在使用转换运算符直接初始化的合法性上存在分歧
Clang and GCC disagree on legality of direct initialization with conversion operator
clang(3.9(的最新版本在f
的第二行拒绝了此代码;最新版本的gcc(6.2(接受它:
struct Y {
Y();
Y(const Y&);
Y(Y&&);
};
struct X {
operator const Y();
};
void f() {
X x;
Y y(x);
}
如果做出任何这些更改,clang将接受代码:
- 删除
Y
的move构造函数 - 从转换运算符中删除
const
- 用
Y y = x
替换Y y(x)
原来的例子合法吗?哪个编译器错了?在检查了标准中关于转换函数和过载解决方案的部分后,我一直未能找到明确的答案。
当我们枚举构造函数并检查它们的可行性时,即是否存在移动构造函数的隐式转换序列时,[dcl.init.ref]/5一直到最后一个要点(5.2.2(,该要点已由核心问题1604和1571(按顺序(修改。
这些决议的底线是
如果
T1
或T2
是类类型,并且T1
不是与T2
相关的引用,则用户定义的转换为考虑通过用户定义的转换使用">cv1T1
"类型对象的复制初始化规则(8.6、13.3.1.4、13.3.1.5(;如果相应的非引用副本初始化将是格式不良的,则程序是格式不良调用转换函数的结果,如非引用副本初始化所述,然后用于直接初始化引用
第一部分只是选择转换运算符。因此,根据黑体部分,我们使用const Y
来直接初始化Y&&
。同样,我们失败了,直到最后一个要点,由于(5.2.2.3(:而失败
如果
T1
是与T2
相关的参考:
--cv1应相同cv资格为,或大于,cv2;和
然而,这不再适用于我们最初的重载解决方案,它只看到转换运算符应用于直接初始化引用。在您的示例中,重载解析选择移动构造函数是因为[over.ics.rank]/(3.2.5(,然后上面的段落使程序格式不正确。这是一个缺陷,已作为核心问题2077提交。一个合理的解决方案是在重载解析期间丢弃move构造函数。
所有这些对您的修复都是有意义的:删除const
可以防止失败,因为类型现在是引用兼容的,删除move构造函数会留下copy构造函数,它有一个const引用(即也可以工作(。最后,当我们编写Y y = x;
时,应用(17.6.3(而不是[dcl.init]/(17.6.2(;
否则(即,对于剩余的复制初始化情况(,如13.3.1.4所述,枚举可以从源类型转换为目标类型或(当使用转换函数时(转换为其派生类的用户定义转换序列,并通过过载解析(13.3(选择最佳转换序列。[…].调用用于直接初始化,根据上面的规则,复制初始化的目标对象
也就是说,初始化实际上与成功的Y y(x.operator const Y());
相同,因为移动构造函数是不可行的(Y&& y = const Y
失败得足够浅(,并且选择了复制构造函数。
我认为这是一个clang错误。
我们从[over.match.ctor]开始:
当类类型的对象被直接初始化时(8.6(,从相同或派生类类型(8.6(或默认初始化(8.6(,重载解析选择构造函数。用于直接初始化或不在复制初始化上下文中的默认初始化,候选函数是正在初始化的对象的类的所有构造函数。
例如,我们考虑复制构造函数。复制构造函数可行吗?
来自[dcl.init.ref]:
--如果初始值设定项表达式[…]具有类类型(即,T2是类类型(,其中T1不是与T2相关的引用,并且可以是转换为类型为"cv3T3"的右值,其中"cv1T1"是与"cv3"兼容的参考T3"(见13.3.1.6(,则引用绑定到第一种情况下的初始值设定项表达式的值,并绑定到第二种情况下的转换结果。
[over.match.ref]中的候选函数是:
对于直接初始化,那些显式转换函数不隐藏在S中,并且收益类型为"对cv2 T2的左值引用"或"对cv2 T2的右值引用"cv2 T2",其中T2与T类型相同,或者可以通过限定转换为T类型转换(4.5(也是候选函数。
其中包括我们的operator const Y()
。因此,复制构造函数是可行的。move构造函数不是(因为你不能将非const
右值引用绑定到const
右值(,所以我们只有一个可行的候选者,这使得程序格式良好。
呃,作为后续,这是LLVM错误16682,这使它看起来比我最初布局的要复杂得多。
当您编写的代码中,两个优秀的编译器对它是否合法存在分歧时,您的编程过于接近边缘。假设我是一名支持该代码的维护程序员。如果连gcc和clang都不能达成一致,你怎么希望我知道这是否合法,以及这段代码的确切语义是什么?
更改代码。让它变得更简单,这样不那么"聪明"的程序员和编译器都能毫无问题地理解它。周围最"聪明"的程序员是没有奖品的。
看看科伦坡的回答:我毫不怀疑他对形势的分析是完全正确的。但我不想支持需要非常聪明的50行分析来证明它是正确的代码。如果你正在编写C++编译器,你应该仔细研究他的答案。如果您正在编写应用程序代码,则永远不应该编写需要查看其答案的代码。
- 是否可以初始化不可复制类型的成员变量(或基类)
- C++使用整数的压缩数组初始化对象
- C++初始化基类
- 多成员Constexpr结构初始化
- 复制列表初始化的隐式转换的等级是多少
- 内联映射初始化的动态atexit析构函数崩溃
- 如何在C++中初始化嵌套类中的2个memeber
- 如何声明特征矩阵,然后通过嵌套循环初始化它
- 没有用于初始化C++中的变量模板的匹配构造函数
- 在未初始化映射的情况下,将值插入到映射的映射中
- C++成员初始化
- 为什么在C++中首先初始化成员类
- 同时具有"聚合初始化"和"模板推导"
- 初始化具有非默认构造函数的std::数组项的更好方法
- 是否可以在编译时初始化数组,以便在运行时不会花费时间?
- 我可以使用条件运算符初始化C风格的字符串文字吗
- 在C和C++中初始化结构中的数组
- 标准是否使用多余的大括号(例如 T{{{10}}})定义列表初始化?
- 在函数内部的声明中初始化数组,并在外部使用它
- Clang和GCC在使用转换运算符直接初始化的合法性上存在分歧