Clang 6,clang 7和gcc 7.1、7.2和7.3都同意以下内容是有效的C ++ 17代码,但在C ++ 14和C ++ 11下却模棱两可。 MSVC 2015和2017也接受它。但是,即使在c ++ 17模式下,gcc-8.1和8.2也会拒绝它:
struct Foo
{
explicit Foo(int ptr);
};
template<class T>
struct Bar
{
operator T() const;
template<typename T2>
explicit operator T2() const;
};
Foo foo(Bar<char> x)
{
return (Foo)x;
}
接受它的编译器会选择模板式显式转换函数Bar::operator T2()
。
拒绝它的编译器同意以下两者之间存在歧义:
Bar<char>
到char
的隐式用户定义转换,然后使用从char
到int
的隐式内置转换,然后使用显式构造函数Foo (int)。那么,哪个编译器正确? C ++ 14和C ++ 17之间的标准有什么区别?
附录:实际错误消息
这是gcc-8.2 -std=c++17
的错误。 gcc-7.2 -std=c++14
显示相同的错误:
<source>: In function 'Foo foo(Bar<char>)':
<source>:17:17: error: call of overloaded 'Foo(Bar<char>&)' is ambiguous
return (Foo)x;
^
<source>:3:14: note: candidate: 'Foo::Foo(int)'
explicit Foo(int ptr);
^~~
<source>:1:8: note: candidate: 'constexpr Foo::Foo(const Foo&)'
struct Foo
^~~
<source>:1:8: note: candidate: 'constexpr Foo::Foo(Foo&&)'
这是clang-7 -std=c++14
中的错误(clang-7 -std=c++17
接受代码):
<source>:17:12: error: ambiguous conversion for C-style cast from 'Bar<char>' to 'Foo'
return (Foo)x;
^~~~~~
<source>:1:8: note: candidate constructor (the implicit move constructor)
struct Foo
^
<source>:1:8: note: candidate constructor (the implicit copy constructor)
<source>:3:14: note: candidate constructor
explicit Foo(int ptr);
^
1 error generated.
答案 0 :(得分:9)
这里有几种力量在起作用。要了解正在发生的事情,让我们检查(Foo)x
应该将我们引向何处。首先,在这种情况下,c样式转换等效于static_cast
。静态类型转换的语义将是直接初始化结果对象。由于结果对象属于类类型,因此[dcl.init]/17.6.2告诉我们它的初始化如下:
否则,如果初始化为直接初始化,或者为 复制初始化,其中源的cv不合格版本 type是与该类相同的类,或者是该类的派生类 目的地,考虑构造函数。适用的构造函数 枚举([over.match.ctor]),然后通过选择最佳的 重载分辨率。如此选择的构造函数称为 使用初始化器表达式初始化对象或 expression-list作为其参数。如果没有构造函数适用,或者 重载解析不明确,初始化格式错误。
因此重载解决方案可以选择要调用的Foo
的构造函数。如果重载解析失败,则程序格式错误。在这种情况下,即使我们有3个候选构造函数,它也不应该失败。它们是Foo(int)
,Foo(Foo const&)
和Foo(Foo&&)
。
首先,我们需要将初始化int
复制为构造函数的参数,这意味着找到从Bar<char>
到int
的隐式转换序列。由于您提供的从Bar<char>
到char
的用户定义的转换运算符不是显式的,因此我们可以将其用于隐式对话序列Bar<char> -> char -> int
中。
对于其他两个构造函数,我们需要将引用绑定到Foo
。但是,我们不能这样做。根据{{3}}:
在[dcl.init.ref]中指定的条件下,引用可以是 直接绑定到作为结果的glvalue或class prvalue 将转换函数应用于初始化程序表达式。超载 resolution用于选择要调用的转换函数。 假设“ cv1 T”是引用的基础类型 已初始化,“ cv S”是初始化表达式的类型, 对于S类类型,候选函数的选择如下:
- 考虑S的转换函数及其基类。那些未隐藏在S中的非显式转换函数 并产生类型“对cv2 T2的左值引用”(初始化 左值引用或函数右值引用)或“ cv2 T2”或 “对cv2 T2的右值引用”(初始化右值引用或 对函数的左值引用),其中“ cv1 T”为 具有“ cv2 T2”的参考兼容([dcl.init.ref]) 功能。对于直接初始化,那些显式转换 未隐藏在S中且产生类型“左值”的函数 引用cv2 T2”或“ cv2 T2”或“右值引用cv2 T2”, 分别,其中T2与T的类型相同或可以转换为 带有资格转换([conv.qual])的T型 候选函数。
唯一可以产生类型Foo
的glvalue或prvalue的转换函数是您指定的显式转换函数模板的特化。但是,由于函数参数的初始化不是直接初始化,因此无法考虑显式转换函数。因此,我们无法以重载分辨率调用副本或移动构造函数。剩下的只有构造函数采用int
的情况。因此,重载解析是成功的,应该如此。
那为什么有些编译器会发现它是模棱两可的,或者调用模板转换运算符呢?好吧,由于保证复制消除已引入标准中,因此指出([over.match.ref]/1)用户定义的转换功能也应有助于复制消除。今天,根据标准的干字母,他们没有。但是我们真的很希望他们这样做。尽管确切的措词仍在制定中,但似乎有些编译器已经着手尝试实现它。
这就是您所看到的实现。扩展复制删除的反作用力在这里干扰了重载分辨率。