当前位置: 首页 > 知识库问答 >
问题:

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

傅边浩
2023-03-14

我有带转换操作符的简单代码,似乎所有的编译器都给出不同的结果,我很好奇哪个编译器是正确的?我也尝试了不同的组合,但以下是最有趣的。代码是使用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 &

g -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 in后

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());

g -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'

共有1个答案

赫连子石
2023-03-14

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

让我们从static_cast开始(§5.2.9 [静态引用]/p4,所有引号都来自N3936):

表达式 e 可以使用 static_cast形式显式转换为 T 类型,static_cast

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

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

请注意,我们将call_operator()重写为call_operator{}只是为了解释目的,因为int t1(call_operator()); 是最麻烦的解析。这两种形式的初始化之间存在很小的语义差异,但这种差异对于本讨论无关紧要。

此初始化的适用规则载于§8.5[dcl.init]/p17:

如果源类型是(可能是cv限定的)类类型,则考虑转换函数。枚举(13.3.1.5)适用的转换函数,并通过重载解析(13.3)选择最佳的转换函数。调用如此选择的用户定义转换将初始化表达式转换为正在初始化的对象。如果转换无法完成或不明确,则初始化格式错误。

我们继续讨论 §13.3.1.5 [过.匹配.conv],其中说:

假设“< code>cv1 T”是正在初始化的对象的类型,并且“< code>cv S”是初始化表达式的类型,其中< code>S是类类型,候选函数选择如下:

  • 考虑了S的转换函数及其基类。那些未隐藏在S中并产生类型T或可通过标准转换序列(13.3.3.1.1)转换为类型T的非显式转换函数是候选函数。对于直接初始化,那些不隐藏在S中并产生类型T的显式转换函数,或者可以通过限定转换(4.4)转换为类型T的类型,也是候选函数。对于选择候选函数的过程,返回cv限定类型的转换函数被视为生成该类型的cv非限定版本。返回“reference to cv2X”的转换函数根据引用类型返回“cv2X”类型的左值或xvalues,因此被视为在选择候选函数的过程中产生X

2参数列表有一个参数,它是初始化表达式。[注意:此参数将与转换函数的隐式对象参数进行比较。-结束注释]

扣除模板参数后,候选集为:

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

参数列表由单个表达式call_operator{}组成,它是非常量的。因此,它转换为运算符T()的非常量隐式对象参数比转换为其他两个更好。因此,operator T()是最佳匹配,并通过重载解析进行选择。

此初始化受 §8.5.3 [目录]/p5 的约束:

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

>

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

    • 是左值(但不是位字段),“cv1 T1”与“cv2T2”或
    • 兼容
    • 具有类类型(即,T2是类类型),其中T1不是与 T3 ”类型的左值,其中“cv1T1”兼容(通过枚举适用的转换函数(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所需的转换

    但是,Clang为< code >运算符T执行的推导似乎

    5通常,推导过程会尝试找到模板参数值,使推导出的AA相同。然而,有两种情况允许存在差异:

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

    6 只有在类型推断否则失败的情况下,才考虑这些备选方案。如果它们产生多个可能的推导 A,则类型推断失败。

    由于类型推导可以通过将 T 推导为运算符 T 的常量 int 来成功

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

    同样,结果中的两个标准转换序列都是标识转换。海湾合作委员会(和EDG,这要归功于@Jonathan韦克利进行测试)正确地推导出操作员T中的T

    然而,不管推导的正确性如何,这里的决胜局都是一样的。因为,根据函数模板的部分排序规则,运算符const T

    F1和F2是函数模板的专门化,根据14.5.6.2中描述的部分排序规则,

    因此,在这种情况下,Clang得到了正确的结果,但(部分)原因是错误的。GCC得到了正确的结果,因为正确的原因。

    这里没有战斗。运算符常量 T

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

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

    请注意,有关对标准转换序列进行排名的规则 - §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:

    如果

    • S1S2的适当子序列(比较13.3.3.1.1定义的规范形式的转换序列,不包括任何Lvalue转换;身份转换序列被认为是任何非身份转换序列的子序列)

    无法区分这两者,因为从const int获取int所需的转换

    因此,唯一可能区分这两个函数的规则又是“更专业”的规则。那么问题是运算符T()运算符const T之一是否

    template<class T> void g(T);
    template<class T> void g(T&);
    

    快速浏览§14.8.2.4 [温度扣除部分]中指定的程序可以确认给定一个模板采取常量T

    * 克朗和海湾合作委员会为运营商T推导出的类型

    简而言之,在部分排序的演绎过程中,在进行任何比较之前,引用类型被替换为被引用的类型,然后顶级cv限定符被去除,因此< code >常量T

    GCC似乎不考虑运算符const T

    这似乎是叮当错误 20783。

  •  类似资料:
    • 考虑以下简单类: 默认的模板化set方法对大多数T类型的值都很好,但我需要对特定类型的行为进行特殊化,将其称为String。 因此,在关联的CPP文件中,我添加了以下内容: 为什么在Xcode中工作而在Visual Studio中不工作 哪个编译器的行为正确,该问题应该被认为是另一个编译器中的bug吗? 顺便说一句,我尝试在头文件中创建一个显式签名: Xcode对此没有抱怨,一切都仍然有效。然而,

    • 考虑下面这个简短的C++程序: 如果我在不同的编译器上编译它,我会得到不同的结果。对于CLANG3.4和GCC 4.4.7,它打印,而Visual Studio 2013打印,这意味着它们在调用不同的强制转换操作符。根据标准,哪一个是正确的行为? 根据我的理解,不需要转换,而需要到的转换,因此编译器应该选择第一个。对此做了什么吗?const-conversion是否被编译器认为更“昂贵”? 如果删

    • 该代码在Coliru中编译,带有警告[Unitized members

    • 在下面的代码中,是一个模板类,取决于非类型参数。为和定义了friend。还依赖于另一个bool模板参数。 在Coliru上看现场直播。 现在,我想给出的模板参数的默认值,例如,以便以下语句 相当于 如果我在 同样,它不编译给出错误 main.cpp:27:15:错误:重新声明friend'template std::ostream&operator<<(std::ostream&,const a&

    • 问题内容: 哪个类首先编译,或者? 需要相同的类作为返回类型并扩展该类。 问题答案: 有很多方法可以实现编译器。但是,基本方法是解析源并构建符号表。然后使用该符号表将源代码转换为目标代码。 单遍编译器只能使用它已经遇到的符号。是旨在使用单遍编译器的语言示例。如果不是不可能的话,使用单遍编译器将很难实现。 但是,大多数语言都使用编译器,因为单遍编译器的优点不再重要,在这种情况下,使用尚未定义的符号变

    • 主要内容:编译JRXML文件,预览Jasper文件我们在上一章中生成了 JasperReport 模板(JRXML 文件)。该文件不能直接用于生成报表。它必须编译为 JasperReport 的原生二进制格式,称为Jasper文件。在编译时,我们将 JasperDesign 对象转换为 JasperReport 对象。 接口net.sf.jasperreports.engine.design.JRCompiler在编译过程中起着核心作用。该接口有