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

Kri*_*ris 24 c++ gcc clang visual-c++ c++11

我有简单的代码与转换运算符,似乎所有编译器给出不同的结果,好奇哪个编译器,如果有的话,是正确的?我尝试了不同的组合,但下面的组合是最有趣的.代码是使用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());
}
Run Code Online (Sandbox Code Playgroud)

铛 - 3.6:

operator int
operator const int &
operator int &
Run Code Online (Sandbox Code Playgroud)

克++ - 4.9:

operator T
operator const T&
operator T&
Run Code Online (Sandbox Code Playgroud)

msvc 2014 CTP:

call_operator.cpp(17): error C2440: 'static_cast': cannot convert from 'call_operator' to ' const int &'
Run Code Online (Sandbox Code Playgroud)

删除后:

template<typename T>
operator T();
Run Code Online (Sandbox Code Playgroud)

msvc编译:

call_operator::operator const int &
call_operator::operator const int &
call_operator::operator int &
Run Code Online (Sandbox Code Playgroud)

此外,删除const后

template<typename T>
operator const T&();
Run Code Online (Sandbox Code Playgroud)

铛 - 3.6:

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

克++ - 4.9:

operator T
operator const T&
operator T&
Run Code Online (Sandbox Code Playgroud)

msvc 2014 CTP:

call_operator.cpp(16): error C2440: 'static_cast': cannot convert from 'call_operator' to 'int'
Run Code Online (Sandbox Code Playgroud)

T.C*_*.C. 22

简而言之:Clang是正确的(尽管在一种情况下,出于错误的原因).GCC在第二种情况下是错误的.MSVC在第一种情况下是错误的.

让我们从static_cast(§5.2.9[expr.static.cast]/p4开始,所有引用来自N3936):

对于某些发明的临时变量(8.5),如果声明格式正确,则e可以T使用 static_cast表单的表达式将表达式显式转换为类型.这种显式转换的效果与执行声明和初始化,然后使用临时变量作为转换结果相同.当且仅当初始化将其用作glvalue时,该表达式才用作glvalue.static_cast<T>(e)T t(e);te

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

int t1(call_operator{});
const int & t2(call_operator{});
int & t3(call_operator{});
Run Code Online (Sandbox Code Playgroud)

请注意,我们重写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或可T通过标准转换序列(13.3.3.1.1)转换为类型的类型是候选函数.对于直接初始化,那些未隐藏在其中的显式转换函数S和yield类型T或可转换为T具有限定转换类型的类型(4.4)也是候选函数.返回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
Run Code Online (Sandbox Code Playgroud)

参数列表由单个表达式组成call_operator{},它是非const的.因此,它更好地转换为非const隐式对象参数而operator T()不是其他两个.因此,operator T()是最佳匹配,并通过重载分辨率选择.

const int & t2(call_operator{});

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

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

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

    • 是左值(但不是位域),"cv1 T1"与"cv2"引用兼容T2,或者
    • 有一个类类型(即,T2是类类型),其中T1与引用无关T2,并且可以转换为类型为"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
Run Code Online (Sandbox Code Playgroud)

很明显,两个函数都绑定在隐式对象参数上,因为它们都是const.此外,由于两者都是直接引用绑定,根据§13.3.3.1.4[ics.ref]/p1,从任一函数的返回类型到const int &标识转换所需的转换.(资格调整 - 指的是§4.4[conv.qual]中描述的转换,仅适用于指针.)

但是,operator T&()在这种情况下,Clang执行的演绎似乎不正确.§14.8.2.3[temp.deduct.conv]/p5-6:

5通常,推导过程会尝试查找模板参数值,这些值将使推导出A相同的值A.但是,有两种情况可以产生差异:

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

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

因为类型推导可以通过推导成功T作为const intoperator T&()用于推导的类型和目标类型之间的精确匹配,该方案不应该被认为,T应该已经推导出const int,和候选集实际上是

operator const T& () const - with T = int
operator T&() const - with T = const int
Run Code Online (Sandbox Code Playgroud)

同样,结果中的两个标准转换序列都是身份转换.海湾合作委员会(和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,第二个列表,最后一个要点:

F1并且F2是函数模板特化,并且函数模板F1F2 根据14.5.6.2中描述的部分排序规则的模板更专业.

因此,在这种情况下,Clang得到了正确的结果,但是(部分地)是错误的原因.由于正当理由,GCC得到了正确的结果.

int & t3(call_operator{});

这里没有战斗.operator const T&();根本无法用于初始化a int &.只有一种可行的功能,operator T&()T = int,所以它是最好的可行函数.

如果operator const T&();不是const

这里唯一有趣的例子是初始化int t1(call_operator{});.两个强有力的竞争者是:

operator T() - with T = int
operator const T& () - with T = int
Run Code Online (Sandbox Code Playgroud)

请注意关于排序标准转换序列的规则 - §13.3.3[over.match.best]/p1,第2列表,第2个要点:

上下文是由用户定义的转换初始化(见8.5,13.3.1.5和13.3.1.6),从返回类型F1到目标类型的标准转换序列(即,被初始化的实体的类型)是转换序列比从返回类型F2到目标类型的标准转换序列更好.

和§13.3.3.2[over.ics.rank]/p2:

标准转换序列S1比标准转换序列更好的转换序列S2,如果

  • S1是一个适当的子S2序列(比较13.3.3.1.1定义的规范形式的转换序列,不包括任何左值变换;身份转换序列被认为是任何非同一性转换序列的子序列)

无法区分这两者,因为int从a 获得a 所需的转换const 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&);
Run Code Online (Sandbox Code Playgroud)

仔细阅读§14.8.2.4[temp.deduct.partial]中规定的程序,确认给定一个模板取a const T&和另一个取一个Tby值,既不比另一个更专业**.因此,在这种情况下,没有唯一的最佳可行功能,转换是模糊的,并且代码是不正确的.


* 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.

  • 所以OP应填写[GCC bugzilla]的错误报告(http://gcc.gnu.org/bugzilla) (2认同)