【问题标题】:Explicit constructors and nested initializer lists显式构造函数和嵌套初始化列表
【发布时间】:2017-11-28 15:16:43
【问题描述】:

以下代码可以使用大多数现代 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 &amp;) = delete;也能编译这个,而其他3个编译器都需要呢?

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

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

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

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

【问题讨论】:

  • 另一个有趣的问题是,当你删除explicit时,GCC会调用A(const char*),而Clang会调用A(std::string)
  • 哪个版本的 MSVC?在过去 7 年左右的时间里,他们在初始化方面遇到了各种怪癖
  • 当 MS 第一次添加移动语义时,他们没有给任何类提供默认的移动构造函数(用户必须声明它们,否则它们不存在)
  • @M.M 使用 v141 (VS2017) 进行了尝试,默认情况下肯定有移动构造函数。也应该适用于 v140。

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


【解决方案1】:

我将尝试解释标准的内容,而不是解释编译器的行为。

主要示例

要从{{{"test"}}} 直接初始化b1,重载决议适用于选择B 的最佳构造函数。因为没有从{{{"test"}}}B&amp; 的隐式转换(列表初始值设定项不是左值),所以构造函数B(B&amp;) 不可行。然后我们关注构造函数B(A),看看是否可行。

要确定从{{{"test"}}}A 的隐式转换顺序(为了简单起见,我将使用符号{{{"test"}}} -> A),重载决议适用于选择A 的最佳构造函数,所以我们需要根据[over.match.list]/1比较{{"test"}}->const char*{{"test"}}->std::string(注意最外层大括号被省略):

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

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

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

...在复制列表初始化中,如果选择了显式构造函数,则初始化格式错误。

请注意,无论说明符 explicit 是什么,这里都会考虑所有构造函数。

{{"test"}} -> 根据[over.ics.list]/10[over.ics.list]/11const char* 不存在:

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

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

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

在除上述列举的情况之外的所有情况下,都无法进行转换。

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

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


变化

如果explicit 被删除会怎样?

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

如果b1的初始化器中只有两层大括号呢?

注意{{"test"}} -> const char* 不存在,但{"test"} -> const char* 存在。所以如果b1的初始化器中只有两层大括号,则选择构造函数A(const char*),因为{"test"}->const char*优于{"test"}->std::string。结果,在copy-list-initialization中选择了显式构造函数(构造函数B(A)中的参数A{"test"}初始化),那么程序是病态的。

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

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

要确定{{{"test"}}} -> const B,采用上述过程。我们需要比较 {{"test"}} -> A{{"test"}} -> const B&amp;。注意{{"test"}} -> const B&amp; 根据[over.best.ics]/4 不存在:

但是,如果目标是

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

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

而构造函数或用户定义的转换函数是候选者

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

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

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

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

为了确定{{"test"}} -> A,再次进行上述过程。这与我们在上一小节中讨论的情况几乎相同。结果,构造函数A(const char*) 被选中。注意这里选择构造函数来确定{{{"test"}}} -> const B,实际并不适用。尽管构造函数是显式的,但这是允许的。

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

如果括号被大括号代替怎么办?

根据前面小节中引用的[over.best.ics]/4,不考虑用户定义的转换{{{"test"}}} -> const B&amp;。因此,即使声明了构造函数B(const B&amp;),结果也与主示例相同。

【讨论】:

    【解决方案2】:

    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* (&amp;&amp;)) {} 并编译。如果它不可用,则需要C(const T* (&amp;&amp;) [N])

    MSVC 只是失败了:

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

    【讨论】:

    • 是的,警告很可能是关于大括号省略的。但是,一旦大括号被嵌套,就不再允许大括号省略,除非a 是标量。因此,在这种情况下,该警告是错误的。添加更多大括号后,不可避免地会调用链中一种非标量类型的复制构造函数。
    【解决方案3】:

    (已编辑,感谢@dyp)

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

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

    1. 一个const char*
    2. 一个std::string(真的是std::basic_string&lt;whatever&gt;
    3. 一个阿
    4. 一个 B

    然后我把它放在GodBolt 上,以 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>
    

    因此,我们看到的正确可操作结构的顺序似乎是:

    (没有看到 1。) 2.std::basic_string::basic_string(const char* /* ignoring the allocator */) 3.A::A(std::string) 4.B::B(A)

    使用 clang 5.0.0,结果与 IIANM 类似,对于 MSVC - 谁知道?也许这是一个错误?众所周知,他们有时在一直正确支持语言标准方面有点狡猾。抱歉,就像我说的 - 部分回答。

    【讨论】:

    • 是吗? B::B(B&&) 呢? 由于用户声明的复制 ctor,那个没有退出 IIRC。
    • 我从clang的警告中猜测是最里面的{}正在初始化char const*
    • (并且用户声明的 copy-ctor 不能绑定到右值,因为它是一个非常量引用,因此 B(B const&amp;) 再次模棱两可)
    • @dyp:你可能是对的。最里面的引用文本是const char[5]。将编辑。
    • @dyp 您可能对用户声明的 copy-ctor 是正确的,即使删除 const 和非成本复制 ctor 之间存在语义差异听起来并不正确。分别表示删除的函数仍然可能不明确。
    猜你喜欢
    • 1970-01-01
    • 2018-06-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-10-24
    • 2012-03-19
    • 1970-01-01
    相关资源
    最近更新 更多