In short: Clang 是正确的(尽管在一种情况下,出于错误的原因)。 GCC 在第二种情况下是错误的。 MSVC 在第一种情况下是错误的。
让我们从static_cast
(§5.2.9 [expr.static.cast]/p4,所有引用均来自 N3936):
一种表达e
可以显式转换为类型T
用一个static_cast
形式的static_cast<T>(e)
如果声明T t(e);
对于某些发明的临时变量来说是格式良好的t
(8.5)。
这种显式转换的效果与执行
声明和初始化,然后使用临时
变量作为转换的结果。表达方式e
用来
当且仅当初始化将其用作左值时,才将其用作左值。
据此,三static_cast
这里实际上是三个初始化:
int t1(call_operator{});
const int & t2(call_operator{});
int & t3(call_operator{});
请注意,我们重写了call_operator()
as call_operator{}
仅供说明之用,如int t1(call_operator());
是最令人烦恼的解析。这两种形式的初始化之间存在细微的语义差异,但这种差异对于本次讨论来说并不重要。
int t1(call_operator{});
§8.5 [dcl.init]/p17 中列出了此初始化的适用规则:
如果源类型是(可能是 cv 限定的)类类型,则转换
功能被考虑。适用的转换函数是
枚举(13.3.1.5),并通过重载选择最好的一个
决议(13.3)。如此选择的用户定义的转换称为
将初始化表达式转换为对象
已初始化。如果转换无法完成或不明确,则
初始化格式不正确。
我们继续进行第 13.3.1.5 节 [over.match.conv],其中表示:
假如说 ”cv1 T
” 是正在初始化的对象的类型,
和 ”cv S
” 是初始化表达式的类型,其中S
A
类类型,候选函数选择如下:
- 的转换函数为
S
及其基类被考虑。那些没有隐藏在其中的非显式转换函数S
和产量类型T
或可以转换为 type 的类型T
通过一个
标准转换序列(13.3.3.1.1)是候选函数。为了
直接初始化,那些显式转换函数
不隐藏在里面S
和产量类型T
或者一个类型可以是
转换为类型T
也有资格转换(4.4)
候选函数。返回 cv 限定的转换函数
类型被认为产生该类型的 cv-unqualified 版本
用于选择候选函数的过程。转换
返回“对 cv2 的引用”的函数X
” 返回左值或
xvalues,取决于引用类型,类型为“cv2X
”并且是
因此认为产生X
对于这个选择的过程
候选函数。
2 参数列表有一个参数,它是初始化器
表达。 [Note:这个论点将与
转换函数的隐式对象参数。 —end note ]
模板实参推导后的候选集为:
operator T() - with T = int
operator const T& () const - with T = int
operator T&() const - with T = int
参数列表由单个表达式组成call_operator{}
,这是非常量。因此,它可以更好地转换为非常量隐式对象参数operator T()
比其他两个。因此,operator T()
是最佳匹配,由重载决策选择。
const int & t2(call_operator{});
此初始化受 §8.5.3 [dcl.init.ref]/p5 管辖:
对类型“cv1”的引用T1
” 由类型表达式初始化
“简历2T2
“ 如下:
-
如果引用是左值引用并且初始化表达式
- 是左值(但不是位字段),并且“cv1 T1”与“cv2”引用兼容
T2
,” or
- 有一个类类型(即
T2
是一个类类型),其中T1
与参考无关T2
,并且可以转换为类型的左值
“CV3T3
,”其中“cv1T1
” 与“cv3 引用兼容T3
”
(此转换是通过枚举适用的转换来选择的
函数(13.3.1.6)并通过重载选择最好的函数
决议(13.3))。
然后引用绑定到初始化表达式左值
第一种情况和转换的左值结果
第二种情况(或者在任何一种情况下,到适当的基类
对象的子对象)。
请注意,此步骤仅考虑返回左值引用的转换函数。
Clang appears to deduce the candidate set as*:
operator const T& () const - with T = int
operator T&() const - with T = int
很明显,这两个函数都依赖于隐式对象参数,因为它们都是const
。此外,由于两者都是直接引用绑定,根据 §13.3.3.1.4 [ics.ref]/p1,需要从任一函数的返回类型转换为const int &
是身份转换。 (Not资格调整 - 指第 4.4 节 [conv.qual] 中描述的转换,仅适用于指针。)
However, it appears that the deduction performed by Clang for operator T&()
in this case is incorrect‡. §14.8.2.3 [temp.deduct.conv]/p5-6:
5 一般来说,推导过程试图找到模板实参
的值将使得推导A
相同A
。然而,有
有两种情况允许存在差异:
- 如果原来的
A
是一个引用类型,A
可以比推导的 A 更符合 cv 条件(即,由
参考)
- 推导出来的
A
可以是另一个指针或指向可转换为的成员类型的指针A
通过资格转换。
6 仅当类型推导能够实现时才考虑这些替代方案
否则失败。如果它们产生不止一种可能的推论A
, 这
类型推导失败。
由于类型推导可以通过推导成功T
as const int
for operator T&()
对于推导类型和目标类型之间的精确匹配,不应考虑替代方案,T
应该被推论为const int
,而候选集实际上是
operator const T& () const - with T = int
operator T&() const - with T = const int
Once again, both standard conversion sequences from the result are identity conversions. GCC (and EDG, thanks to @Jonathan Wakely for testing) correctly deduces T
in operator T&()
to be const int
in this case*.
然而,无论推论的正确性如何,这里的决胜局都是一样的。因为,根据函数模板的偏序规则,operator const T& ()
比更专业operator T&()
(由于§14.8.2.4 [temp.deduct.partial]/p9中的特殊规则),前者在§13.3.3 [over.match.best]/p1中通过决胜局获胜,第二个列表,最后一个要点:
F1
and F2
是函数模板特化,并且函数
模板为F1
比模板更专业F2
根据 14.5.6.2 中描述的部分排序规则。
因此,在这种情况下,Clang 得到了正确的结果,但原因(部分)错误。 GCC 出于正确的原因得到了正确的结果。
int & t3(call_operator{});
这里没有战斗。operator const T&();
根本不可能用于初始化int &
。只有一个可行的功能,operator T&()
with T = int
,所以它是最好的可行函数。
What if operator const T&();
isn't const
?
这里唯一有趣的情况是初始化int t1(call_operator{});
。两个强有力的竞争者是:
operator T() - with T = int
operator const T& () - with T = int
请注意,有关标准转换序列排名的规则 - §13.3.3 [over.match.best]/p1,第二个列表,第二个要点:
上下文是通过用户定义的转换进行的初始化(参见 8.5,
13.3.1.5 和 13.3.1.6) 以及来自返回类型的标准转换序列F1
到目标类型(即
正在初始化的实体)是比
从返回类型的标准转换序列F2
到
目的地类型。
和 §13.3.3.2 [over.ics.rank]/p2:
标准转换顺序S1
是一个比以下更好的转换序列
标准转换顺序S2
if
-
S1
是一个真子序列S2
(比较 13.3.3.1.1 定义的规范形式的转换序列,不包括任何
左值变换;考虑身份转换序列
是任何非恒等转换序列的子序列)
无法区分这两者,因为获得一个所需的转换int
from a const int &
is an 左值到右值转换,这是左值转换。排除左值变换后,从结果到目标类型的标准转换序列是相同的; §13.3.3.2 [over.ics.rank] 中的任何其他规则也不适用。
因此,可能区分这两个功能的唯一规则仍然是“更专业”的规则。那么问题是是否其中之一operator T()
and operator const T&()
比另一个更专业。答案是不。详细的部分排序规则相当复杂,但在 §14.5.6.2 [temp.func.order]/p2 的示例中很容易找到类似的情况,该示例将调用标记为g(x)
由于含糊不清:
template<class T> void g(T);
template<class T> void g(T&);
A quick perusal of the procedure specified in §14.8.2.4 [temp.deduct.partial] confirms that given one template taking a const T&
and the other taking a T
by value, neither is more specialized than the other**. Thus, in this case, there is no unique best viable function, the conversion is ambiguous, and the code is ill-formed.†
* The type deduced by Clang and GCC for the operator T&()
case is determined by running the code with operator const T&()
removed.
** Briefly, during the deduction for partial ordering, before any comparison is done, reference types are replaced with the types referred to, and then top-level cv-qualifiers are stripped, so both const T&
and T
yield the same signature. However, §14.8.2.4 [temp.deduct.partial]/p9 contains a special rule for when both types at issue were reference types, which makes operator const T&()
more specialized than operator T&()
; that rule doesn't apply when one of the types is not a reference type.
† GCC appears to not consider operator const T&()
a viable conversion for this case, but does consider operator T&()
a viable conversion.
‡ This appears to be Clang bug 20783 http://llvm.org/bugs/show_bug.cgi?id=20783.