繁体   English   中英

Clang vs GCC vs MSVC模板转换运算符 - 哪个编译器是对的?

[英]Clang vs GCC vs MSVC template conversion operator - which compiler is right?

我有简单的代码与转换运算符,似乎所有编译器给出不同的结果,好奇哪个编译器,如果有的话,是正确的? 我尝试了不同的组合,但下面的组合是最有趣的。 代码是使用C ++ 11标志编译的,但在C ++ 03中也可能会出现相同的行为。

#include <iostream>

struct call_operator {
    template<typename T>
    operator T() {
        std::cout << __FUNCTION__ << std::endl;
        return {};
    }

    template<typename T>
    operator const T&() const {
        std::cout << __FUNCTION__ << std::endl;
        static T t;
        return t;
    }

    template<typename T>
    operator T&() const {
        std::cout << __FUNCTION__ << std::endl;
        static T t;
        return t;
    }
};

int main() {
    (void)static_cast<int>(call_operator());
    (void)static_cast<const int&>(call_operator());
    (void)static_cast<int&>(call_operator());
}

铛 - 3.6:

operator int
operator const int &
operator int &

克++ - 4.9:

operator T
operator const T&
operator T&

msvc 2014 CTP:

call_operator.cpp(17): error C2440: 'static_cast': cannot convert from 'call_operator' to ' const int &'

删除后:

template<typename T>
operator T();

msvc编译:

call_operator::operator const int &
call_operator::operator const int &
call_operator::operator int &

此外,删除const后

template<typename T>
operator const T&();

铛 - 3.6:

call_operator.cpp:26:9: error: ambiguous conversion for static_cast from 'call_operator' to 'int' (void)static_cast<int>(call_operator());

克++ - 4.9:

operator T
operator const T&
operator T&

msvc 2014 CTP:

call_operator.cpp(16): error C2440: 'static_cast': cannot convert from 'call_operator' to 'int'

简而言之 :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),其形式良好。 这种显式转换的效果与执行声明和初始化,然后使用临时变量作为转换结果相同。 当且仅当初始化将其用作glvalue时,表达式e用作glvalue。

因此,这里的三个static_cast实际上是三个初始化:

int t1(call_operator{});
const int & t2(call_operator{});
int & t3(call_operator{});

请注意,我们将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和yield类型T非显式转换函数或可通过标准转换序列(13.3.3.1.1)转换为类型T类型是候选函数。 对于直接初始化,那些未隐藏在S和yield类型T显式转换函数或者可以通过限定转换(4.4)转换为类型T类型也是候选函数。 返回cv限定类型的转换函数被认为是为此选择候选函数的过程产生该类型的cv非限定版本。 返回“引用cv2 X ”的转换函数返回lvalues或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{} ,它是非const的。 因此,它更好地转换为operator T()的非const隐式对象参数,而不是其他两个。 因此, operator T()是最佳匹配,并通过重载决策来选择。

const int & t2(call_operator{});

此初始化由§8.5.3[dcl.init.ref] / p5管理:

对类型“cv1 T1 ”的引用由类型“cv2 T2 ”的表达式初始化,如下所示:

  • 如果引用是左值引用和初始化表达式

    • 是左值(但不是位域),“cv1 T1”与“cv2 T2 ”引用兼容,或者
    • 具有类类型(即, T2是类类型),其中T1T2不是引用相关的,并且可以转换为类型为“cv3 T3 ”的左值,其中“cv1 T1 ”与“cv3”引用兼容T3 “(通过枚举适用的转换函数(13.3.1.6)并通过重载决策(13.3)选择最佳转换函数来选择此转换)。

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

请注意,此步骤仅考虑返回左值引用的转换函数。

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]中描述的转换,仅适用于指针。)

然而,似乎由锵用于执行的扣operator T&()在这种情况下是不正确‡。 §14.8.2.3[temp.deduct.conv] / p5-6:

5一般来说,推导过程试图找到模板参数值,这将使推导出A相同的A 但是,有两种情况可以产生差异:

  • 如果原始A是引用类型,则A可以比推导出的A(即引用所引用的类型)更符合cv标准。
  • 推导出的A可以是另一个指向成员类型的指针或指针,可以通过限定转换转换为A

6只有在类型扣除失败的情况下才考虑这些替代方案。 如果它们产生多于一个可能推导出的A ,则类型推断失败。

因为类型推导可以推断成功Tconst intoperator T&()用于推导的类型和目标类型之间的精确匹配,替代品不应被视为, T应该被推断为const int ,而候选集实际上是

operator const T& () const - with T = int
operator T&() const - with T = const int

同样,结果中的两个标准转换序列都是身份转换。 GCC(和EDG,由于@Jonathan Wakely用于测试)正确地推导出Toperator T&()const int在这种情况下*。

然而,无论扣除的正确性如何,这里的决胜局都是一样的。 因为,根据函数模板的部分排序规则, operator const T& ()operator T&()更专用(由于§14.8.2.4[temp.deduct.partial] / p9中的特殊规则),前者获胜由§13.3.3中的决胜局[over.match.best] / p1,第2个清单,最后一个要点:

F1F2是功能模板特化,根据14.5.6.2中描述的偏序规则, F1的功能模板比F2的模板更专业。

因此,在这种情况下,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是比标准转换序列S2更好的转换序列

  • S1S2的正确子序列(比较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限定符被剥离,因此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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM