C++ 显式构造函数和嵌套初始值设定项列表

C++ 显式构造函数和嵌套初始值设定项列表,c++,c++11,overload-resolution,list-initialization,C++,C++11,Overload Resolution,List Initialization,下面的代码使用大多数现代C++11兼容的编译器(GCC>=5.x、Clang、ICC、MSVC)成功编译 (编辑,谢谢@dyp) 这是一个部分的回答和推测,解释我是如何解释发生的事情的,不是编译专家,而不是C++大师。 首先,我会用一些直觉和常识。显然,最后发生的事情是aB::B(a),因为这是b1唯一可用的构造函数(显然它不可能是aB::B(B&&),因为至少定义了一个副本构造函数,所以B::B(B&)不是为我们隐式定义的)。此外,A或B的第一个构造不能是A::A(const char*),因

下面的代码使用大多数现代C++11兼容的编译器(GCC>=5.x、Clang、ICC、MSVC)成功编译

(编辑,谢谢@dyp)

这是一个部分的回答和推测,解释我是如何解释发生的事情的,不是编译专家,而不是C++大师。 首先,我会用一些直觉和常识。显然,最后发生的事情是a

B::B(a)
,因为这是b1唯一可用的构造函数(显然它不可能是a
B::B(B&&)
,因为至少定义了一个副本构造函数,所以
B::B(B&)
不是为我们隐式定义的)。此外,A或B的第一个构造不能是
A::A(const char*)
,因为它是显式的,所以必须使用
A::A(std::string)
。此外,最里面引用的文本是
常量字符[5]
。所以我猜第一个最里面的构造是
常量char*
;然后是字符串构造:
std::string::string(const char*)
。还有一个花括号结构,我猜它是
A::A(A&&&)
(或者可能是
A::A(A&)
?)。所以,总结一下我的直觉猜测,结构的顺序应该是:

  • A
    const char*
  • 一个
    std::string
    (实际上是
    std::basic_string
  • A
  • a B
  • 然后我把它放在上面,GCC作为第一个例子。(或者,您可以自己编译它,同时保留汇编语言输出,并通过
    c++filt
    传递它以使其更具可读性)。以下是C++中特别提到的代码:

    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>
    
    呼叫4006a0
    致电400858
    致电400868
    打400680
    致电400690
    致电400690
    
    因此,我们看到的适当可操作结构的顺序似乎是:

    (没有看到1。) 2. <代码>标准::基本字符串::基本字符串(常量字符*/*忽略分配器*/) 3. <代码>A::A(标准::字符串) 4. <代码>B::B(A)

    对于Clang5.0.0,结果与IIANM相似,至于MSVC,谁知道呢?也许是虫子?大家都知道,他们有时在一路正确支持语言标准方面有点狡猾。对不起,就像我说的-部分回答。

    b1({{{“test”}}})
    类似于
    b1(A{std::string{const char*[1]{“test”}})

    16.3.3.1.5列表初始化顺序

    否则,如果参数类型是字符数组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"}}});
    }
    

    我不想解释编译器的行为,而是想解释一下标准是怎么说的

    主要示例 要从
    {{{“test”}}
    直接初始化
    b1
    ,重载解析应用于选择
    B
    的最佳构造函数。由于没有从
    {{{“test”}}
    B&
    (列表初始值设定项不是左值)的隐式转换,因此构造函数
    B(B&)
    不可行。然后我们关注构造函数
    B(A)
    ,并检查它是否可行

    为了确定从
    {{“test”}}}
    A
    的隐式转换序列(为了简单起见,我将使用符号
    {{“test”}}}
    ->
    A
    ),重载解析应用于选择
    A
    的最佳构造函数,因此我们需要比较
    {“test”}
    ->
    {{“test”}
    ->
    std::string
    (注意大括号的最外层被省略),根据:

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

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

    • 如果没有找到可行的初始值设定项列表构造函数,将再次执行重载解析,其中候选函数是类T的所有构造函数,参数列表由初始值设定项列表的元素组成

    …在复制列表初始化中,如果选择了显式构造函数,则初始化格式不正确

    注意:这里考虑所有构造函数,而不考虑说明符
    explicit

    {{“test”}
    ->
    const char*
    根据和不存在:

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

    • 如果初始值设定项列表有一个本身不是初始值设定项列表的元素

    • 如果初始值设定项列表没有元素

    在除上述情况外的所有情况下,不可能进行转换

    要确定
    {“test”}
    ->
    std::string
    ,将采用相同的过程,重载解析将选择
    std::string
    的构造函数,该构造函数采用
    const char*
    类型的参数

    因此,
    {{{“test”}}
    ->
    a
    是通过选择构造函数
    a(std::string)
    来完成的


    变化 如果删除了显式
    ,该怎么办? 这个过程没有改变。GCC将选择构造函数
    A(const char*)
    ,而Clang将选择构造函数
    A(std::string)
    。我认为这是GCC的一个错误

    如果
    b1
    的初始值设定项中只有两层大括号怎么办? 注意
    {“test”}
    ->
    常量字符*
    不存在,但
    {“test”}
    ->
    常量字符*
    存在。因此如果
    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>
    
    #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"}}});
    }
    
    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 ) {}
            ^
    
    29 : <source>(29): error C2664: 'B::B(B &)': cannot convert argument 1 from 'initializer list' to 'A'