【问题标题】:Clang vs GCC vs MSVC template conversion operator - which compiler is right?Clang vs GCC vs MSVC 模板转换运算符 - 哪个编译器是正确的?
【发布时间】:2014-11-02 22:36:19
【问题描述】:

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

clang-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 后
template<typename T>
operator const T&();

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

【问题讨论】:

  • 不错的测试,但为什么会有几个重载的模板化转换运算符??
  • 我将它用于自动依赖注入 - 检查 std::is_constructible 并使用 some_type(call_operator()...) 创建类型
  • 我可能读错了,但看起来 gcc 和 clang 给你的结果是一样的。他们只是以不同的方式命名函数,但由于 __FUNCTION__ 不是标准定义的宏,我认为这是意料之中的。
  • 是的,在第一次测试 gcc 和 clang 给出了相同的结果并且 msvc 没有编译,在第二个(从 operator const T&() 中删除 const)gcc 给出了与以前相同的结果,clang 和 msvc没有编译
  • EDG 在所有情况下都与 Clang 一致(如果我将 __FUNCTION__ 的使用替换为字符串文字,那么输出在实现之间是一致的)

标签: c++ visual-c++ gcc c++11 clang


【解决方案1】:

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

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

表达式 e 可以显式转换为类型 T,使用 static_cast 的形式 static_cast&lt;T&gt;(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 &amp; t2(call_operator{});

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

对类型“cv1 T1”的引用由类型为的表达式初始化 “cv2T2”如下:

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

    • 是一个左值(但不是位域),并且“cv1 T1”与“cv2 T2”是引用兼容的,或者
    • 具有类类型(即T2是类类型),其中T1T2没有引用相关,可以转换为类型的左值 “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 &amp; 所需的转换是身份转换。 (不是限定性调整——指的是§4.4 [conv.qual]中描述的转换,并且只适用于指针。)

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

5 一般情况下,演绎过程会尝试查找模板参数 使推导的AA 相同的值。然而,有 是允许不同的两种情况:

  • 如果原始A 是引用类型,A 可以比推导的 A 更具有 cv 限定(即,由 参考)
  • 推导出的A 可以是另一个指针或指向成员类型的指针,可以通过限定转换将其转换为A

6 只有当类型推导能够 否则失败。如果它们产生多个可能的推导A,则 类型推导失败。

由于类型推导可以通过推导 Toperator T&amp;() 来成功推导 operator T&amp;() 以在推导类型和目标类型之间进行精确匹配,因此不应该考虑替代方案,T 应该被推导为const int,而候选集其实是

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

再一次,来自结果的两个标准转换序列都是身份转换。 GCC(和 EDG,感谢 @Jonathan Wakely 的测试)正确地推断出 operator T&amp;() 中的 T 在这种情况下是 const int*

不管推论的正确性如何,这里的决胜局都是一样的。因为,根据函数模板的偏序规则,operator const T&amp; ()operator T&amp;() 更专业(由于 §14.8.2.4 [temp.deduct.partial]/p9 中的特殊规则),前者以决胜局获胜在 §13.3.3 [over.match.best]/p1,第二个列表,最后一个要点:

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

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

int &amp; t3(call_operator{});

这里没有战斗。 operator const T&amp;(); 根本不可能用于初始化 int &amp;。只有一个可行的函数,operator T&amp;()T = int,所以它是最好的可行函数。

如果operator const T&amp;(); 不是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

  • S1S2 的适当子序列(比较 13.3.3.1.1 定义的规范形式的转换序列,不包括任何 左值变换;考虑恒等转换序列 作为任何非身份转换序列的子序列)

无法区分这两个,因为从const int &amp; 获得int 所需的转换是左值到右值 转换,它是左值转换。排除左值变换后,从结果到目的类型的标准转换序列是相同的; §13.3.3.2 [over.ics.rank] 中的任何其他规则也不适用。

因此,唯一可能区分这两个功能的规则还是“更专业”的规则。那么问题是operator T()operator const T&amp;() 中的一个是否比另一个更专业。答案是不。详细的偏序规则相当复杂,但在 §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&amp; 而另一个采用 T 值,两者都没有比另一个更专业**。因此,在这种情况下,没有唯一的最佳可行函数,转换不明确,代码格式不正确。


* Clang 和 GCC 为 operator T&amp;() 情况推导的类型是通过运行删除了 operator const T&amp;() 的代码来确定的。

** 简而言之,在偏序推导过程中,在进行任何比较之前,将引用类型替换为所引用的类型,然后剥离顶级 cv-qualifiers ,所以const T&amp;T 都产生相同的签名。但是,§14.8.2.4 [temp.deduct.partial]/p9 包含一个特殊规则,当有争议的两种类型都是引用类型时,这使得 operator const T&amp;()operator T&amp;() 更专业;当其中一种类型不是引用类型时,该规则不适用。

GCC 似乎不认为 operator const T&amp;() 在这种情况下是可行的转换,但确实认为 operator T&amp;() 是可行的转换。

这似乎是 Clang bug 20783

【讨论】:

  • 所以 OP 应该在 GCC bugzilla 上填写错误报告
  • @BasileStarynkevitch 实际上,这三个实现似乎都有问题,但方式不同......
  • +1 非常好。很难做到这一点(程序员更难预见)这一事实可以说是 C++ 的一个缺点。或者,换种说法,有些水域是程序员不应该冒险的,因为即使标准是由编译器实现的,它们也很难驾驭,但在实践中却无法驾驭。
猜你喜欢
  • 2016-09-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-08-21
  • 1970-01-01
  • 2022-10-12
  • 2017-10-17
相关资源
最近更新 更多