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 将接受代码:

原来的例子合法吗?哪个编译器错了?在检查了标准中有关转换函数和重载解析的部分后,我未能找到明确的答案。

我认为这是一个 clang 错误。

我们从[over.match.ctor]开始:

When objects of class type are direct-initialized (8.6), copy-initialized from an expression of the same or a derived class type (8.6), or default-initialized (8.6), overload resolution selects the constructor. For direct-initialization or default-initialization that is not in the context of copy-initialization, the candidate functions are all the constructors of the class of the object being initialized.

所以我们考虑,例如,复制构造函数。复制构造函数可行吗?

来自[dcl.init.ref]:

— If the initializer expression [...] has a class type (i.e., T2 is a class type), where T1 is not reference-related to T2, and can be converted to an rvalue of type “cv3 T3”, where “cv1 T1” is reference-compatible with “cv3 T3” (see 13.3.1.6) then the reference is bound to the value of the initializer expression in the first case and to the result of the conversion in the second case.

[over.match.ref]中的那些候选函数是:

For direct-initialization, those explicit conversion functions that are not hidden within S and yield type “lvalue reference to cv2 T2” or “cv2 T2” or “rvalue reference to cv2 T2”, respectively, where T2 is the same type as T or can be converted to type T with a qualification conversion (4.5), are also candidate functions.

其中包括我们的operator const Y()。因此复制构造函数 可行的。移动构造函数不是(因为你不能将非 const 右值引用绑定到 const 右值),所以我们只有一个可行的候选者,这使得程序格式良好。


呃,作为后续,这是 LLVM bug 16682,这使得它看起来比我最初布置的要复杂得多。

当我们枚举构造函数并检查它们的可行性时 - 即是否存在隐式转换序列 - 对于移动构造函数,[dcl.init.ref]/5 falls through to the last bullet point (5.2.2), which was modified by core issues 1604 and 1571(按此顺序)。

这些决议的底线是

If T1 or T2 is a class type and T1 is not reference-related to T2, user-defined conversions are considered using the rules for copy-initialization of an object of type “cv1 T1” by user-defined conversion (8.6, 13.3.1.4, 13.3.1.5); the program is ill-formed if the corresponding non-reference copy-initialization would be ill-formed. The result of the call to the conversion function, as described for the non-reference copy-initialization, is then used to direct-initialize the reference.

第一部分只是导致选择转换运算符。因此,根据粗体部分,我们使用const Y直接初始化Y&&。同样,我们失败了,直到最后一个要点,由于 (5.2.2.3) 而失败:

If T1 is reference-related to T2:
cv1 shall be the same cv-qualification as, or greater cv-qualification than, cv2 ; and

然而,这不再属于我们最初的重载决议,它只看到转换运算符应用于直接初始化引用。在您的示例中,重载决议选择移动构造函数,因为 [over.ics.rank]/(3.2.5), and then the above paragraph makes the program ill-formed. This is a defect and has been filed as core issue 2077。明智的解决方案是在重载解析期间丢弃移动构造函数。

所有这些对于您的修复都是有意义的:删除 const 将防止失败,因为类型现在是引用兼容的,并且删除移动构造函数会留下具有常量引用的复制构造函数(即也有效)。最后,当我们写 Y y = x; 而不是 [dcl.init]/(17.6.2) 时,(17.6.3) 适用;

Otherwise (i.e., for the remaining copy-initialization cases), user-defined conversion sequences that can convert from the source type to the destination type or (when a conversion function is used) to a derived class thereof are enumerated as described in 13.3.1.4, and the best one is chosen through overload resolution (13.3). [...]. The call is used to direct-initialize, according to the rules above, the object that is the destination of the copy-initialization.

即初始化实际上与成功的 Y y(x.operator const Y()); 相同,因为移动构造函数不可行(Y&& y = const Y 失败足够浅)并且选择了复制构造函数。

当你编写的代码在两个体面的编译器不同意它是否合法的情况下,你的编程太接近边缘了。假设我是支持该代码的维护程序员。如果连 gcc 和 clang 都不一致,你怎么指望我知道这是否合法,以及这段代码的语义到底是什么?

更改您的代码。让它更简单,这样 less "clever" 程序员和编译器都能毫无问题地理解它。成为最多 "clever" 程序员是没有奖品的。

看看哥伦布的回答:我毫不怀疑他对形势的分析是完全正确的。但我不想支持需要非常聪明的 50 行分析来证明它是正确的代码。如果你正在编写 C++ 编译器,你应该仔细研究他的答案。如果您正在编写应用程序代码,则永远不要编写需要查看他的答案的代码。