C ++ 17:显式转换函数与显式构造函数+隐式转换-规则是否已更改?

时间:2018-11-01 12:17:31

标签: c++ c++14 language-lawyer c++17 implicit-conversion

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

拒绝它的编译器同意以下两者之间存在歧义:

  1. 显式转换函数Bar :: operator int()
  2. 首先使用从Bar<char>char的隐式用户定义转换,然后使用从charint的隐式内置转换,然后使用显式构造函数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.

1 个答案:

答案 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)用户定义的转换功能也应有助于复制消除。今天,根据标准的干字母,他们没有。但是我们真的很希望他们这样做。尽管确切的措词仍在制定中,但似乎有些编译器已经着手尝试实现它。

这就是您所看到的实现。扩展复制删除的反作用力在这里干扰了重载分辨率。