繁体   English   中英

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

[英]Explicit constructors and nested initializer lists

以下代码可使用大多数现代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;情况下进行编译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"}}});

与其解释编译器的行为,不如解释该标准的含义。

主要例子

为了从{{{"test"}}}直接初始化b1 ,重载解析适用于选择B的最佳构造函数。 因为没有从{{{"test"}}}B&隐式转换(列表初始化器不是左值),所以构造函数B(B&)不可行。 然后,我们专注于构造函数B(A) ,并检查它是否可行。

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

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

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

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

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

请注意,无论指定符explicit是什么,这里都考虑所有构造explicit

根据[over.ics.list] / 10[over.ics.list] / 11, {{"test"}} -> const 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 结果,在复制列表初始化(从{"test"}初始化构造函数B(A)中的参数A中选择了一个显式构造函数,然后该程序格式错误。

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

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

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

但是,如果目标是

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

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

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

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

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

[over.match.list]的第二阶段,当初始值设定项列表恰好具有一个本身就是初始值设定项列表的元素,并且目标是类X的构造函数的第一个参数,并且转换为X或对简历 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& 因此,即使声明了构造函数B(const B&) ,结果也与主要示例相同。

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'

(编辑,谢谢@dyp)

这是部分答案和推测,它解释了我如何解释发生的事情,而不是作为一名编译专家并且没有太多C ++ Guru。

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

  1. const char*
  2. 一个std::string (实际上是std::basic_string<whatever>
  3. 一个A

然后我将其放在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来说也是如此-谁知道? 也许是个错误? 他们有时在完全支持语言标准方面有些不知所措。 抱歉,就像我说的-部分答案。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM