C++ 中转换运算符到值和常量引用之间的重载解析

Overload resolution between conversion operators to value and to const-reference in C++

在下面的程序中,struct B 定义了两个转换运算符:to A 和 to const A&。然后 A-object 从 B-object:

创建
struct A {};

struct B {
  A a;
  B() = default;
  operator const A&() { return a; }
  operator A() { return a; }
};

int main() {
  (void) A(B{});
}

节目是

GCC 错误信息是

error: call of overloaded 'A(B)' is ambiguous
note: candidate: 'constexpr A::A(const A&)'
note: candidate: 'constexpr A::A(A&&)'

这里是哪个编译器?

实施分歧可能与CWG 2327有关。

如果严格看C++20的写法,GCC是对的,重载决议是模棱两可的。我先详细说一下措辞,然后在回答的最后再讨论CWG 2327。

初始化有两个候选:

A::A(const A&);
A::A(A&&);

第一步是确定调用每个候选项所需的隐式转换顺序:从“B的右值”到const A&的ICS,以及从“[=11的右值”的ICS =]" 到 A&&B 的值类别实际上并不相关,因为 B 中的转换函数都没有 ref-qualifier.

要从 B 转换为 const A&A&&,我们转到 [dcl.init.ref]。对于 const A& 的转换,p5.1.2 适用:

A reference to type “cv1 T1” is initialized by an expression of type “cv2 T2” as follows:

  • If the reference is an lvalue reference and 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 lvalue of type “cv3 T3”, where “cv1 T1” is reference-compatible with “cv3 T3” (this conversion is selected by enumerating the applicable conversion functions (12.4.2.7) and choosing the best one through overload resolution (12.4)),

    then the reference is bound to the initializer expression lvalue in the first case and to the lvalue result of the conversion in the second case (or, in either case, to the appropriate base class subobject of the object).

这适用,因为 B 可以转换为 const A 类型的左值(这是 T3),并且 const A(如 T1 ) 是 reference-compatible 和 const A(如 T3)。

要从B转换为A&&,适用的规则是p5.3.2,除了这次我们只寻找产生某种类型的右值的转换函数外,它非常相似T3.

12.4.2.7,a.k.a。 [over.match.ref] 解释了如何找到候选转换函数:

[...] Those non-explicit conversion functions that are not hidden within S and yield type “lvalue reference to cv2 T2” (when initializing an lvalue reference or an rvalue reference to function) or “cv2 T2” or “rvalue reference to cv2 T2” (when initializing an rvalue reference or an lvalue reference to function), where “cv1 T” is reference-compatible (9.4.4) with “cv2 T2”, are candidate functions. [...]

在初始化const A&时,显然operator const A&()是候选之一。 operator A() 不是候选者,因为它不产生左值。 (const A& 可以从 A 右值初始化这一事实是无关紧要的;从上面的措辞可以看出,[[=148] 中的 const 引用没有特殊的大小写=]].) 当初始化 A&& 时,operator A() 是候选者,而 operator const A&() 不是,因为它不产生右值。

因此,我们有以下隐式转换序列:

  • 对于候选人 A::A(const A&),ICS 是 B{} 上的临时物化转换,然后是 user-defined 转换 B::operator const A&,最后是身份转换。
  • 对于候选人 A::A(A&&),ICS 是 B{} 上的临时物化转换,然后是 user-defined 转换 B::operator A,最后是身份转换。

排序user-defined转换序列[over.ics.rank]/3.3的基本规则是,如果两个ICS使用相同的user-defined转换函数,第二个标准转换的ICS顺序更好被认为是整体更好的 ICS。这条规则在这里不适用,因为这两个转换函数不同。本节 p4 中的 tie-breaker 规则并不偏爱其中一个。所以最后我们必须去 [over.match.best.general] 中的全局 tie-breaker 规则。 p2.2 似乎可能相关:

  • Given these definitions, a viable function F1 is defined to be a better function than another viable function F2 if for all arguments i, ICSi(F1) is not a worse conversion sequence than ICSi(F2), and then
  • the context is an initialization by user-defined conversion (see 9.4, 12.4.2.6, and 12.4.2.7) and the standard conversion sequence from the return type of F1 to the destination type (i.e., the type of the entity being initialized) is a better conversion sequence than the standard conversion sequence from the return type of F2 to the destination type or, if not that, [...]

但是,对措辞的正确理解表明,这些规则也不会挑出任何一个构造函数优于另一个构造函数。尽管我们的重载决议涉及 select user-defined 转换函数的“子任务”重载决议,但这些子任务已经完成,我们不再处于 user-defined 初始化的“上下文”中转换;我们正处于 select 最佳构造函数的背景下。

所以没有规则可以用来 select 一个构造函数优于另一个构造函数;重载解析失败。

早期标准版本中似乎没有任何允许编译的措辞。

但是如果您查看链接页面上对 CWG 2327 的讨论,您会发现 Richard Smith 建议对初始化规则进行更改。在当前规则下,由于 A 是 class 类型,重载决议总是涉及枚举 A 的构造函数并选择最佳候选者,我们在上面讨论过,这可能涉及“子任务” " 考虑从 BA 的构造函数所需的类型的转换函数。 Smith 非正式地提议 B 的转换函数与 A 的构造函数 一起被考虑在顶层 。但是,目前没有建议的措辞来解释如何根据构造函数对此类转换函数进行排名。

如果有三个可能的初始化候选,即

  • 调用A::A(const A&)(其中B{}必须隐式转换为const A&
  • 调用A::A(A&&)(其中B{}必须隐式转换为A&&
  • 直接致电B::operator A

那么第三个选项被认为比其他两个更好是合理的,我怀疑史密斯已经提出了一些规则并在 Clang 中实现了它,但我不确定它是什么.我敢肯定,一旦他解决了措辞的所有情况,他就会为问题添加措辞。如果情况确实如此,那么 Clang 仅在 C++17 模式及更高版本中接受代码(通过调用 operator A 而不是构造函数)是有道理的,其中保证 cop省略适用。至于 MSVC,也许他们有一个提议的解决方案,他们决定一直应用到 C++14(也可能是 C++11)。