C++ 中转换运算符到值和常量引用之间的重载解析
在下面的程序中,struct B
定义了两个转换运算符:to A
和 to const A&
。然后从B
-对象创建A
-对象:
struct A {};
struct B {
A a;
B() = default;
operator const A&() { return a; }
operator A() { return a; }
};
int main() {
(void) A(B{});
}
该程序
- 被所有语言版本的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&&)'
哪个编译器就在这里吗?
In the following program struct B
defines two conversion operators: to A
and to const A&
. Then A
-object is created from B
-object:
struct A {};
struct B {
A a;
B() = default;
operator const A&() { return a; }
operator A() { return a; }
};
int main() {
(void) A(B{});
}
The program is
- accepted by MSVC in all language versions.
- rejected by GCC in all language versions.
- rejected by Clang in C++14 mode and accepted in C++17 mode.
Demo: https://gcc.godbolt.org/z/rKv589EG3
GCC error message is
error: call of overloaded 'A(B)' is ambiguous
note: candidate: 'constexpr A::A(const A&)'
note: candidate: 'constexpr A::A(A&&)'
Which compiler is right here?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
实现差异可能与 CWG 2327 有关。
如果严格看C++20的措辞,那么GCC是正确的,并且重载决议是不明确的。我将首先详细讨论措辞,然后在答案的末尾我将再次讨论 CWG 2327。
初始化有两个候选:
第一步是确定调用每个候选所需的隐式转换序列:从“
B
的右值”到const A&
的 ICS ,以及从“B
的右值”到A&&
的 ICS。不过,B
的值类别实际上并不相关,因为B
中的转换函数都没有引用限定符。要从
B
转换为const A&
或A&&
,我们转到 [dcl.init.ref]。对于到const A&
的转换,p5.1.2 适用:这是因为
B
可以转换为const A
类型的左值(这是T3
),并且const A
code>(如T1
)与const A
(如T3
)引用兼容。要从
B
转换为A&&
,适用的规则是 p5.3.2,它非常相似,只是这次我们只寻找产生某种类型T3
的右值。12.4.2.7,又名 [over.match.ref] 解释了如何查找候选转换函数:
当初始化
const A&
时,显然operator const A&()
是候选者之一。运算符 A()
不是候选者,因为它不产生左值。 (const A&
可以从A
右值初始化这一事实是无关紧要的;正如您从上面的措辞中看到的,没有特殊的大小写[over.match.ref] 中的 const
引用。)初始化A&&
时,operator A()
是候选者,而operator常量A&()
不是,因为它不产生右值。因此,我们有以下隐式转换序列:
A::A(const A&)
,ICS 是B{}
上的临时物化转换,然后是用户定义的转换B::operator const A&
以及最后的恒等转换。A::A(A&&)
,ICS 是B{}
上的临时具体化转换,后跟用户定义的转换B ::operator A
最后是身份转换。对用户定义转换序列进行排序的基本规则[over.ics.rank]/3.3是,如果两个ICS使用相同的用户定义转换函数,则认为第二个标准转换序列更好的一个为整体更好的工业控制系统。此规则在这里不适用,因为两个转换函数不同。本节第 4 页中的决胜规则并不偏向其中之一。所以最后我们必须讨论[over.match.best.general]中的全局决胜规则。 p2.2 似乎可能相关:
但是,对措辞的正确理解表明,这些规则不会选择任何一个构造函数作为也比另一个好。尽管我们的重载决议涉及“子任务”重载决议来选择用户定义的转换函数,但这些子任务已经完成,我们不再处于用户定义的转换初始化的“上下文”中;我们正在选择最好的建造者。
因此,没有任何规则可用于选择任一构造函数而不是另一个构造函数;重载决策失败。
早期标准版本中似乎没有任何允许编译的措辞。
但如果您查看链接页面上对 CWG 2327 的讨论,您会发现 Richard Smith 建议对初始化规则进行更改。在当前规则下,由于
A
是类类型,因此重载决策始终涉及枚举A
的构造函数并选择最佳候选者,我们上面讨论过,这可能涉及: “子任务”考虑从B
到A
构造函数所需类型的转换函数。 Smith 非正式地提议将B
的转换函数与A
的构造函数一起考虑在顶层。然而,目前还没有建议的措辞来解释如何根据构造函数对此类转换函数进行排名。如果存在三种可能的初始化候选,即
A::A(const A&)
(其中B{}
必须隐式转换为const A& ;
)A::A(A&&)
(其中B{}
必须隐式转换为A&&
代码>)B::operator A
直接那么第三个选项被认为比其他两个更好是合理的,我怀疑 Smith 已经提出了一些规则并在 Clang 中实现了它,但是我不确定它是什么。我相信,一旦他解决了措辞的所有情况,他就会为该问题添加措辞。如果确实如此,那么 Clang 仅在适用保证复制省略的 C++17 模式及更高版本中接受代码(通过调用运算符 A 而不是构造函数)是有意义的。至于 MSVC,也许他们有一个提议的解决方案,并决定将其应用到 C++14(也可能是 C++11)。
The implementation divergence is probably related to CWG 2327.
If look strictly at the wording of C++20, then GCC is right and the overload resolution is ambiguous. I'll go into the wording in detail first, and then at the end of the answer I'll discuss CWG 2327 again.
There are two candidates for the initialization:
The first step is to determine the implicit conversion sequence required to call each candidate: the ICS from "rvalue of
B
" toconst A&
, and the ICS from "rvalue ofB
" toA&&
. The value category of theB
is not actually relevant, though, because neither conversion function inB
has a ref-qualifier.To convert from
B
toconst A&
or toA&&
, we go to [dcl.init.ref]. For the conversion toconst A&
, p5.1.2 applies:This applies because
B
can be converted to an lvalue of typeconst A
(this is theT3
), andconst A
(asT1
) is reference-compatible withconst A
(asT3
).To convert from
B
toA&&
, the applicable rule is p5.3.2, which is very similar except that this time we are only looking for conversion functions that yield an rvalue of some typeT3
.12.4.2.7, a.k.a. [over.match.ref] explains how to find the candidate conversion functions:
When initializing
const A&
, obviouslyoperator const A&()
is one of the candidates.operator A()
is not a candidate since it doesn't yield an lvalue. (The fact that aconst A&
can be initialized from anA
rvalue is irrelevant; as you can see from the wording above, there is no special casing forconst
references in [over.match.ref].) When initializingA&&
,operator A()
is a candidate, andoperator const A&()
is not, because it doesn't yield an rvalue.Thus, we have the following implicit conversion sequences:
A::A(const A&)
, the ICS is a temporary materialization conversion onB{}
followed by the user-defined conversionB::operator const A&
and finally an identity conversion.A::A(A&&)
, the ICS is a temporary materialization conversion onB{}
followed by the user-defined conversionB::operator A
and finally an identity conversion.The basic rule for ranking user-defined conversion sequences, [over.ics.rank]/3.3, is that if two ICSes use the same user-defined conversion function, the one whose second standard conversion sequence is better is considered to be the overall better ICS. This rule doesn't apply here because the two conversion functions are different. The tie-breaker rules in p4 of this section do not prefer one over the other. So finally we have to go to the global tie-breaker rules in [over.match.best.general]. p2.2 seems like it might be relevant:
However, a proper understanding of the wording reveals that these rules don't pick out either constructor as being better than the other, either. Although our overload resolution involved a "subtask" overload resolution to select user-defined conversion functions, those subtasks have already been completed and we are no longer in the "context" of an initialization by user-defined conversion; we are in the context of selecting the best constructor.
So there is no rule that can be used to select either constructor over the other; the overload resolution fails.
There does not seem to be any wording from earlier standard editions that would allow this to compile.
But if you look at the discussion of CWG 2327 on the linked page, you'll see that Richard Smith suggests that a change be made to the initialization rules. Under the current rules, since
A
is a class type, the overload resolution always involves enumerating constructors ofA
and picking the best candidate, which we discussed above, which might involve as "subtasks" the consideration of conversion functions fromB
to types required byA
's constructors. Smith has informally proposed that the conversion functions ofB
be considered at the top level alongside the constructors ofA
. However, there is currently no proposed wording explaining how to rank such conversion functions against constructors.If there are three possible candidates for the initialization, namely
A::A(const A&)
(whereB{}
must be implicitly converted toconst A&
)A::A(A&&)
(whereB{}
must be implicitly converted toA&&
)B::operator A
directlythen it would be reasonable for the third option to be considered better than the other two, and I suspect that Smith has already come up with some rule and implemented it in Clang, but I'm not sure what it is. I'm sure he'll add wording to the issue once he's worked out all the cases for the wording. If this is indeed the case, then it makes sense that Clang accepts the code (by calling
operator A
instead of a constructor) only in C++17 mode and later, where guarantee copy elision applies. As for MSVC, perhaps they have a proposed resolution that they've decided to apply all the way back to C++14 (and probably C++11 as well).