以下代码成功编译了大多数现代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"}}});
答案 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&&)
?)。因此,总结一下我的直观猜测,结构的顺序应该是:
A::A(A&)
const char*
(真的是std::string
)然后我把它放在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 - 谁知道?也许这是一个错误?众所周知,他们在完全支持语言标准方面有点狡猾。对不起,就像我说的那样 - 部分回答。