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

Fed*_*dor 9 c++ conversion-operator language-lawyer overload-resolution

在下面的程序中 structB定义了两个转换运算符: toA和 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{});
}
Run Code Online (Sandbox Code Playgroud)

该计划是

  • MSVC 接受所有语言版本。
  • 所有语言版本均被 GCC 拒绝。
  • 在 C++14 模式下被 Clang 拒绝,在 C++17 模式下被接受。演示: https: //gcc.godbolt.org/z/rKv589EG3

GCC 错误消息是

error: call of overloaded 'A(B)' is ambiguous
note: candidate: 'constexpr A::A(const A&)'
note: candidate: 'constexpr A::A(A&&)'
Run Code Online (Sandbox Code Playgroud)

这里是哪个编译器?

Bri*_*ian 1

实现差异可能与CWG 2327有关。

\n

如果严格看C++20的措辞,那么GCC是正确的,并且重载决议是不明确的。我将首先详细讨论措辞,然后在答案的末尾我将再次讨论 CWG 2327。

\n

初始化有两个候选者:

\n
A::A(const A&);\nA::A(A&&);\n
Run Code Online (Sandbox Code Playgroud)\n

第一步是确定调用每个候选项所需的隐式转换序列:从“rvalue of B” 到 的ICS,以及从“rvalue of ” 到 的const A&ICS 。不过,的值类别实际上并不相关,因为 中 的转换函数都没有ref -qualifierBA&&BB

\n

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

\n
\n

对类型 \xe2\x80\x9c cv1 \xe2\x80\x9d 的引用由类型 \xe2\x80\x9c cv2T1 \xe2\x80\x9d的表达式初始化,如下所示: T2

\n
    \n
  • 如果引用是左值引用并且初始化表达式

    \n
      \n
    • [...]
    • \n
    • 具有类类型(即T2是类类型),其中T1与 不引用相关T2,并且可以转换为 \xe2\x80\x9c cv3 T3 \xe2\x80\x9d 类型的左值,其中 \xe2\x80\ x9c cv1 \xe2\x80\x9d 与 \xe2\x80\x9c cv3T1 \xe2\x80\x9d引用兼容(通过枚举适用的转换函数 (12.4.2.7) 并通过重载解析选择最佳转换来选择此转换) (12.4)), T3
    • \n
    \n

    那么该引用在第一种情况下绑定到初始值设定项表达式左值,在第二种情况下绑定到转换的左值结果(或者在任何一种情况下,绑定到该对象的相应基类子对象)。

    \n
  • \n
\n
\n

这是适用的,因为B可以转换为类型的左值const A(这是T3),并且const A(as ) 与(as )T1引用兼容。const AT3

\n

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

\n

12.4.2.7,又名 [over.match.ref] 解释了如何查找候选转换函数:

\n
\n

[...] 那些未隐藏并S产生类型 \xe2\x80\x9clvalue 对cv2 T2 \xe2\x80\x9d 的引用的非显式转换函数(当初始化对函数的左值引用或右值引用时)或 \xe2 \x80\x9c cv2 T2 \xe2\x80\x9d 或 \xe2\x80\x9crvalue 对cv2 T2 \xe2\x80\x9d 的引用(初始化对函数的右值引用或左值引用时),其中 \xe2\x80\x9c cv1 T \ xe2\x80\x9d 与 \xe2\x80\x9c 引用兼容(9.4.4)cv2 T2 \xe2\x80\x9d 是候选函数。[...]

\n
\n

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

\n

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

\n
    \n
  • 对于候选人 来说A::A(const A&),ICS 是临时物化转换,B{}然后是用户定义的转换B::operator const A&,最后是身份转换。
  • \n
  • 对于候选人 来说A::A(A&&),ICS 是临时物化转换,B{}然后是用户定义的转换B::operator A,最后是身份转换。
  • \n
\n

对用户定义转换序列进行排序的基本规则[over.ics.rank]/3.3是,如果两个ICS使用相同的用户定义转换函数,则认为第二个标准转换序列更好的一个为整体更好的工业控制系统。此规则不适用于此处,因为两个转换函数不同。本节第 4 页中的决胜规则并不偏爱其中之一。所以最后我们必须讨论[over.match.best.general]中的全局决胜规则。p2.2 似乎可能相关:

\n
\n
    \n
  • 给定这些定义,如果对于所有参数i来说,一个可行函数F1被定义为比另一个可行函数更好的函数,ICS i ( ) 不是比 ICS i ( ) 更差的转换序列,然后F2F1F2
  • \n
  • 上下文是通过用户定义的转换进行的初始化(参见 9.4、12.4.2.6 和 12.4.2.7),从返回类型F1到目标类型(即正在初始化的实体的类型)的标准转换序列是比从返回类型F2到目标类型的标准转换序列更好的转换序列,或者,如果不是这样,[...]
  • \n
\n
\n

然而,对措辞的正确理解表明,这些规则也不会选出任何一个构造函数比另一个更好。尽管我们的重载决议涉及“子任务”重载决议来选择用户定义的转换函数,但这些子任务已经完成,我们不再处于用户定义的转换初始化的“上下文”中;我们正在选择最好的建造者。

\n

因此,没有任何规则可用于选择任一构造函数而不是另一个构造函数;重载决策失败。

\n

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

\n

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

\n

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

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

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

\n