简而言之: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
当且仅当初始化将其用作 glvalue 时,作为 glvalue。
因此,这里的三个static_casts 实际上是三个初始化:
int t1(call_operator{});
const int & t2(call_operator{});
int & t3(call_operator{});
请注意,我们将call_operator() 重写为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
类类型,候选函数选择如下:
- 考虑
S及其基类的转换函数。那些不隐藏在S中的非显式转换函数
并产生类型 T 或可以通过 a 转换为类型 T 的类型
标准转换序列(13.3.3.1.1)是候选函数。为了
直接初始化,那些显式转换函数
不隐藏在 S 和产量类型 T 或可以是
通过限定转换 (4.4) 转换为类型 T 也是
候选函数。返回 cv 限定的转换函数
type 被认为产生该类型的 cv 非限定版本
对于这个选择候选函数的过程。转换
返回“对 cv2 X 的引用”的函数返回左值或
xvalues,取决于引用的类型,类型为“cv2 X”,并且是
因此被认为在这个选择过程中产生X
候选函数。
2 参数列表有一个参数,即初始化器
表达。 [ 注意:这个参数将与
转换函数的隐式对象参数。 —尾注 ]
模板参数推导后的候选集为:
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”的引用由类型为的表达式初始化
“cv2T2”如下:
-
如果引用是左值引用和初始化表达式
- 是一个左值(但不是位域),并且“cv1 T1”与“cv2
T2”是引用兼容的,或者
- 具有类类型(即
T2是类类型),其中T1与T2没有引用相关,可以转换为类型的左值
“cv3 T3”,其中“cv1 T1”与“cv3 T3”引用兼容
(通过枚举适用的转换来选择此转换
函数(13.3.1.6)并通过重载选择最好的函数
决议 (13.3))。
然后引用绑定到初始化表达式左值
第一种情况和转换的左值结果
第二种情况(或者,在任何一种情况下,到适当的基类
对象的子对象)。
请注意,此步骤仅考虑返回左值引用的转换函数。
Clang 似乎将候选集推断为*:
operator const T& () const - with T = int
operator T&() const - with T = int
很明显,这两个函数都绑定在隐式对象参数上,因为它们都是const。此外,由于两者都是直接引用绑定,根据§13.3.3.1.4 [ics.ref]/p1,从任一函数的返回类型到const int & 所需的转换是身份转换。 (不是限定性调整——指的是§4.4 [conv.qual]中描述的转换,并且只适用于指针。)
但是,在这种情况下,Clang 对operator T&() 的推演似乎是不正确的‡。 §14.8.2.3 [temp.deduct.conv]/p5-6:
5 一般情况下,演绎过程会尝试查找模板参数
使推导的A 与A 相同的值。然而,有
是允许不同的两种情况:
- 如果原始
A 是引用类型,A 可以比推导的 A 更具有 cv 限定(即,由
参考)
- 推导出的
A 可以是另一个指针或指向成员类型的指针,可以通过限定转换将其转换为A。
6 只有当类型推导能够
否则失败。如果它们产生多个可能的推导A,则
类型推导失败。
由于类型推导可以通过推导 T 为 operator T&() 来成功推导 operator T&() 以在推导类型和目标类型之间进行精确匹配,因此不应该考虑替代方案,T 应该被推导为const int,而候选集其实是
operator const T& () const - with T = int
operator T&() const - with T = const int
再一次,来自结果的两个标准转换序列都是身份转换。 GCC(和 EDG,感谢 @Jonathan Wakely 的测试)正确地推断出 operator T&() 中的 T 在这种情况下是 const int*。
不管推论的正确性如何,这里的决胜局都是一样的。因为,根据函数模板的偏序规则,operator const T& () 比 operator T&() 更专业(由于 §14.8.2.4 [temp.deduct.partial]/p9 中的特殊规则),前者以决胜局获胜在 §13.3.3 [over.match.best]/p1,第二个列表,最后一个要点:
F1 和 F2 是函数模板特化,函数
F1 的模板比 F2 的模板更专业
根据 14.5.6.2 中描述的部分排序规则。
因此,在这种情况下,Clang 得到了正确的结果,但由于(部分)错误的原因。 GCC 得到了正确的结果,出于正确的原因。
int & t3(call_operator{});
这里没有战斗。 operator const T&(); 根本不可能用于初始化 int &。只有一个可行的函数,operator T&() 和 T = int,所以它是最好的可行函数。
如果operator const T&(); 不是const 怎么办?
这里唯一有趣的例子是初始化int t1(call_operator{});。两个强有力的竞争者是:
operator T() - with T = int
operator const T& () - with T = int
注意关于标准转换序列排名的规则 - §13.3.3 [over.match.best]/p1, 2nd list, 2nd bullet point:
上下文是用户定义转换的初始化(见 8.5,
13.3.1.5 和 13.3.1.6)以及从 F1 的返回类型到目标类型(即
实体被初始化)是一个比
从F2 的返回类型到
目的地类型。
和§13.3.3.2 [over.ics.rank]/p2:
标准转换序列S1 是比标准转换序列更好的转换序列
标准转换序列S2if
-
S1 是 S2 的适当子序列(比较 13.3.3.1.1 定义的规范形式的转换序列,不包括任何
左值变换;考虑恒等转换序列
作为任何非身份转换序列的子序列)
无法区分这两个,因为从const int & 获得int 所需的转换是左值到右值 转换,它是左值转换。排除左值变换后,从结果到目的类型的标准转换序列是相同的; §13.3.3.2 [over.ics.rank] 中的任何其他规则也不适用。
因此,唯一可能区分这两个功能的规则还是“更专业”的规则。那么问题是operator T() 和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&);
快速浏览 §14.8.2.4 [temp.deduct.partial] 中指定的程序可以确认,给定一个模板采用 const T& 而另一个采用 T 值,两者都没有比另一个更专业**。因此,在这种情况下,没有唯一的最佳可行函数,转换不明确,代码格式不正确。†
* Clang 和 GCC 为 operator T&() 情况推导的类型是通过运行删除了 operator const T&() 的代码来确定的。
** 简而言之,在偏序推导过程中,在进行任何比较之前,将引用类型替换为所引用的类型,然后剥离顶级 cv-qualifiers ,所以const T& 和T 都产生相同的签名。但是,§14.8.2.4 [temp.deduct.partial]/p9 包含一个特殊规则,当有争议的两种类型都是引用类型时,这使得 operator const T&() 比 operator T&() 更专业;当其中一种类型不是引用类型时,该规则不适用。
† GCC 似乎不认为 operator const T&() 在这种情况下是可行的转换,但确实认为 operator T&() 是可行的转换。
‡ 这似乎是 Clang bug 20783。