显式构造函数和嵌套初始化列表

时间:2017-11-28 15:16:43

标签: c++ c++11 overload-resolution list-initialization

以下代码成功编译了大多数现代C ++ 11兼容编译器(GCC> = 5.x,Clang,ICC,MSVC)。

#include <string>

struct A
{
        explicit A(const char *) {}
        A(std::string) {}
};

struct B
{
        B(A) {}
        B(B &) = delete;
};

int main( void )
{
        B b1({{{"test"}}});
}

但是为什么它首先编译,以及列出的编译器如何解释该代码?

为什么MSVC能够在没有B(B &) = delete;的情况下编译它,但其他3个编译器都需要它?

当我删除复制构造函数的 不同签名 时,除了MSVC之外,为什么它在所有编译器中都失败了,例如: B(const B &) = delete;

编译器是否都选择了相同的构造函数?

为什么Clang会发出以下警告?

17 : <source>:17:16: warning: braces around scalar initializer [-Wbraced-scalar-init]
        B b1({{{"test"}}});

3 个答案:

答案 0 :(得分:5)

我不会解释编译器的行为,而是试图解释标准所说的内容。

主要示例

要从b1直接初始化{{{"test"}}},重载决策适用于选择B的最佳构造函数。由于没有从{{{"test"}}}B&的隐式转换(列表初始值设定项不是左值),因此构造函数B(B&)不可行。然后,我们将重点放在构造函数B(A)上,并检查它是否可行。

要确定从{{{"test"}}}A的隐式转换序列(为简单起见,我将使用符号{{{"test"}}} - &gt; A),重载决策适用于选择A的最佳构造函数,因此我们需要比较{{"test"}} - &gt; const char*{{"test"}} - &gt;根据{{​​3}} std::string(请注意,最外层的大括号已被省略):

  

当非聚合类类型T的对象被列表初始化时,[dcl.init.list]指定根据本子条款中的规则执行重载决策,重载决策分两个阶段选择构造函数:

     
      
  • 最初,候选函数是类T的初始化列表构造函数([dcl.init.list])...

  •   
  • 如果找不到可行的初始化列表构造函数,则再次执行重载解析,其中候选函数是类T的所有构造函数,参数列表由初始化列表的元素组成。

  •   
     

...在copy-list-initialization中,如果选择了显式构造函数,则初始化是错误的。

注意,无论指定符explicit如何,都会考虑所有构造函数。

{{"test"}} - &gt;根据{{​​3}}和[over.match.list]/1

const char*不存在
  

否则,如果参数类型不是类:

     
      
  • 如果初始化列表中有一个元素本身不是初始化列表 ...

  •   
  • 如果初始化列表没有元素......

  •   
     

除上述列举的所有情况外,不得进行任何转换。

确定{{"test"}} - &gt; std::string,采用相同的过程,重载决策选择std::string的构造函数,该构造函数采用const char*类型的参数。

因此,{{{"test"}}} - &gt; A是通过选择构造函数A(std::string)来完成的。

的变化

如果删除explicit怎么办?

该过程不会改变。 GCC将选择构造函数A(const char*),而Clang将选择构造函数A(std::string)。我认为这是GCC的一个错误。

如果b1的初始化程序中只有两层大括号,该怎么办?

注意{{"test"}} - &gt; const char*不存在{"test"} - &gt; const char*存在。因此,如果初始值设定项b1中只有两层大括号,则选择构造函数A(const char*),因为{"test"} - &gt; const char*优于{"test"} - &gt; std::string。因此,在复制列表初始化(从A构造函数B(A)中初始化参数{"test"})中选择了显式构造函数,然后程序格式不正确。 / p>

如果声明了构造函数B(const B&),该怎么办?

请注意,如果删除了B(B&)的声明,也会发生这种情况。这次我们需要比较{{{"test"}}} - &gt; A{{{"test"}}} - &gt; const B&{{{"test"}}} - &gt;等价const B

确定{{{"test"}}} - &gt; const B,采用上述过程。我们需要比较{{"test"}} - &gt; A{{"test"}} - &gt; const B&。注意{{"test"}} - &gt;根据{{​​3}}

const B&不存在
  

但是,如果目标是

     

- 构造函数的第一个参数或

     

- 用户定义的转换函数的隐式对象参数

     

,构造函数或用户定义的转换函数是

的候选函数      

- [over.match.ctor],当参数是类复制初始化的第二步中的临时参数时,

     

- [over.match.copy],[over.match.conv]或[over.match.ref](在所有情况下)或

     

- [over.match.list]的第二阶段,当初始化列表只有一个元素本身就是一个初始化列表时,目标是类X的构造函数的第一个参数,并且转换是X或   参考 cv X

     

不考虑用户定义的转换序列。

确定{{"test"}} - &gt; A,再次采用上述过程。这几乎与我们在前一小节中谈到的情况相同。结果,选择了构造函数A(const char*)。请注意,此处选择构造函数来确定{{{"test"}}} - &gt; const B,但实际上并不适用。虽然构造函数是显式的,但这是允许的。

因此,{{{"test"}}} - &gt;通过选择构造函数const B,然后选择构造函数B(A)来完成A(const char*)。现在两个{{{"test"}}} - &gt; A{{{"test"}}} - &gt; const B是用户定义的转换序列,两者都不比另一个好,因此b1的初始化是不明确的。

如果用括号替换括号怎么办?

根据前一小节中引用的[over.ics.list]/10,用户定义的转化{{{"test"}}} - &gt; const B&未被考虑。因此,即使构造函数B(const B&)被声明,结果也与主要示例相同。

答案 1 :(得分:1)

B b1({{{"test"}}});B b1(A{std::string{const char*[1]{"test"}}});

类似
  

16.3.3.1.5列表初始化序列[over.ics.list]

     

4否则,如果参数类型是字符数组133并且初始化列表具有单个元素,该元素是适当类型的字符串文字(11.6.2),则隐式转换序列是标识转换。

编译器会尝试所有可能的隐式转换。例如,如果我们使用以下构造函数的C类:

#include <string>

struct C
{
    template<typename T, size_t N>     C(const T* (&&) [N]) {}
    template<typename T, size_t N>     C(const T  (&&) [N]) {}
    template<typename T=char>         C(const T* (&&)) {}
    template<typename T=char>          C(std::initializer_list<char>&&) {}
};

struct A
{
    explicit A(const char *) {}

    A(C ) {}
};

struct B
{
    B(A) {}
    B(B &) = delete;
};

int main( void )
{
    const char* p{"test"};
    const char p2[5]{"test"};

    B b1({{{"test"}}});
}

clang 5.0.0编译器无法决定使用和失败:

29 : <source>:29:11: error: call to constructor of 'C' is ambiguous
    B b1({{{"test"}}});
          ^~~~~~~~~~
5 : <source>:5:40: note: candidate constructor [with T = char, N = 1]
    template<typename T, size_t N>     C(const T* (&&) [N]) {}
                                       ^
6 : <source>:6:40: note: candidate constructor [with T = const char *, N = 1]
    template<typename T, size_t N>     C(const T  (&&) [N]) {}
                                       ^
7 : <source>:7:39: note: candidate constructor [with T = char]
    template<typename T=char>         C(const T* (&&)) {}
                                      ^
15 : <source>:15:9: note: passing argument to parameter here
    A(C ) {}
        ^

但是如果我们只留下一个非初始化列表构造函数,则代码编译得很好。

GCC 7.2只选择C(const T* (&&)) {}并进行编译。如果它不可用则需C(const T* (&&) [N])

MSVC失败了:

29 : <source>(29): error C2664: 'B::B(B &)': cannot convert argument 1 from 'initializer list' to 'A'

答案 2 :(得分:0)

(已编辑,感谢@dyp)

这是一个部分答案和推测,解释了我如何解释发生的事情,而不是编译专家而不是C ++大师。

首先,我会有一些直觉和常识。显然,最后发生的事情是B::B(A),因为这是B b1唯一可用的构造函数(显然它不能是B::B(B&&),因为至少定义了一个复制构造函数,所以{{1}不是为我们隐式定义的)。此外,要发生的A或B的第一个构造不能是B::B(B&&)因为那个是明确的,所以必须使用A::A(const char*)。此外,最里面的引用文本是A::A(std::string)。所以我猜第一个,最里面的结构是const char[5];然后是字符串构造:const char*。还有一个花括号结构,我猜它是std::string::string(const char *)(或者A::A(A&&)?)。因此,总结一下我的直观猜测,结构的顺序应该是:

  1. A A::A(A&)
  2. const char*(真的是std::string
  3. a A
  4. a B
  5. 然后我把它放在GodBolt上,GCC作为第一个例子。 (或者,您可以在保持汇编语言输出的同时自己编译它,并通过std::basic_string<whatever>传递它以使其更具可读性)。以下是专门提到C ++代码的所有行:

    c++filt

    所以看起来我们看到的正确可操作结构的顺序是:

    (没看到1.) 2. call 4006a0 <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(char const*, std::allocator<char> const&)@plt> call 400858 <A::A(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)> call 400868 <B::B(A)> call 400680 <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()@plt> call 400690 <std::allocator<char>::~allocator()@plt> call 400690 <std::allocator<char>::~allocator()@plt> 3. std::basic_string::basic_string(const char* /* ignoring the allocator */) 4. A::A(std::string)

    使用clang 5.0.0,结果类似于IIANM,而对于MSVC - 谁知道?也许这是一个错误?众所周知,他们在完全支持语言标准方面有点狡猾。对不起,就像我说的那样 - 部分回答。