显式构造函数和嵌套的初始化列表
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;
的情况下编译它,但其他 3 个编译器都需要它?
当我删除复制构造函数的不同签名时,为什么它在除 MSVC 之外的所有编译器中都失败,例如B(const B &) = delete;
?
编译器甚至都选择相同的构造函数吗?
为什么 Clang 发出以下警告?
17 : <source>:17:16: warning: braces around scalar initializer [-Wbraced-scalar-init]
B b1({{{"test"}}});
(已编辑,感谢@dyp)
这是部分答案和推测,解释了我是如何解释发生的事情的,不是编译专家,也不是 C++ 大师。
首先,我将借助一些直觉和常识。显然,最后发生的事情是 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&)
?)。所以,总结一下我的直觉猜测,构造顺序应该是:
- 一个
const char*
- 一个
std::string
(实际上是std::basic_string<whatever>
)
- 一个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 - 谁知道呢?也许这是一个错误?众所周知,他们有时在正确支持语言标准方面有点狡猾。抱歉,就像我说的 - 部分回答。
B b1({{{"test"}}});
就像 B b1(A{std::string{const char*[1]{"test"}}});
16.3.3.1.5 List-initialization sequence [over.ics.list]
4 Otherwise, if the parameter type is a character array 133 and the initializer list has a single element that is an appropriately-typed string literal (11.6.2), the implicit conversion sequence is the identity conversion.
并且编译器会尝试所有可能的隐式转换。例如,如果我们有 class 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'
我将尝试解释标准的内容,而不是解释编译器的行为。
主要示例
要从 {{{"test"}}}
直接初始化 b1
,重载决策适用于选择 B
的最佳构造函数。因为没有从 {{{"test"}}}
到 B&
的隐式转换(列表初始值设定项不是左值),所以构造函数 B(B&)
不可行。然后我们关注构造函数B(A)
,并检查它是否可行。
为了确定从{{{"test"}}}
到A
的隐式转换序列(为了简单起见,我将使用符号{{{"test"}}}
-> A
),重载决议适用于选择A
的最佳构造函数,所以我们需要比较 {{"test"}}
-> const char*
和 {{"test"}}
-> std::string
(注意最外层的大括号被省略了)根据 [over.match.list]/1:
When objects of non-aggregate class type T are list-initialized such that [dcl.init.list] specifies that overload resolution is performed according to the rules in this subclause, overload resolution selects the constructor in two phases:
Initially, the candidate functions are the initializer-list constructors ([dcl.init.list]) of the class T...
If no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class T and the argument list consists of the elements of the initializer list.
... In copy-list-initialization, if an explicit constructor is chosen, the initialization is ill-formed.
请注意,无论说明符如何,此处都会考虑所有构造函数 explicit
。
{{"test"}}
-> const char*
根据 [over.ics.list]/10 and [over.ics.list]/11 不存在:
Otherwise, if the parameter type is not a class:
if the initializer list has one element that is not itself an initializer list...
if the initializer list has no elements...
In all cases other than those enumerated above, no conversion is possible.
要确定{{"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中选择了显式构造函数(从{"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"}}
-> 根据 [over.best.ics]/4:
const B&
不存在
However, if the target is
— the first parameter of a constructor or
— the implicit object parameter of a user-defined conversion function
and the constructor or user-defined conversion function is a candidate by
— [over.match.ctor], when the argument is the temporary in the second step of a class copy-initialization,
— [over.match.copy], [over.match.conv], or [over.match.ref] (in all cases), or
— the second phase of [over.match.list] when the initializer list has exactly one element that is itself an initializer list, and the target is the first parameter of a constructor of class X, and the conversion is to X or
reference to cv X,
user-defined conversion sequences are not considered.
确定{{"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&)
,结果也与主要示例相同。
以下代码可以使用大多数现代 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"}}});
(已编辑,感谢@dyp)
这是部分答案和推测,解释了我是如何解释发生的事情的,不是编译专家,也不是 C++ 大师。
首先,我将借助一些直觉和常识。显然,最后发生的事情是 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&)
?)。所以,总结一下我的直觉猜测,构造顺序应该是:
- 一个
const char*
- 一个
std::string
(实际上是std::basic_string<whatever>
) - 一个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 - 谁知道呢?也许这是一个错误?众所周知,他们有时在正确支持语言标准方面有点狡猾。抱歉,就像我说的 - 部分回答。
B b1({{{"test"}}});
就像 B b1(A{std::string{const char*[1]{"test"}}});
16.3.3.1.5 List-initialization sequence [over.ics.list]
4 Otherwise, if the parameter type is a character array 133 and the initializer list has a single element that is an appropriately-typed string literal (11.6.2), the implicit conversion sequence is the identity conversion.
并且编译器会尝试所有可能的隐式转换。例如,如果我们有 class 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'
我将尝试解释标准的内容,而不是解释编译器的行为。
主要示例
要从 {{{"test"}}}
直接初始化 b1
,重载决策适用于选择 B
的最佳构造函数。因为没有从 {{{"test"}}}
到 B&
的隐式转换(列表初始值设定项不是左值),所以构造函数 B(B&)
不可行。然后我们关注构造函数B(A)
,并检查它是否可行。
为了确定从{{{"test"}}}
到A
的隐式转换序列(为了简单起见,我将使用符号{{{"test"}}}
-> A
),重载决议适用于选择A
的最佳构造函数,所以我们需要比较 {{"test"}}
-> const char*
和 {{"test"}}
-> std::string
(注意最外层的大括号被省略了)根据 [over.match.list]/1:
When objects of non-aggregate class type T are list-initialized such that [dcl.init.list] specifies that overload resolution is performed according to the rules in this subclause, overload resolution selects the constructor in two phases:
Initially, the candidate functions are the initializer-list constructors ([dcl.init.list]) of the class T...
If no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class T and the argument list consists of the elements of the initializer list.
... In copy-list-initialization, if an explicit constructor is chosen, the initialization is ill-formed.
请注意,无论说明符如何,此处都会考虑所有构造函数 explicit
。
{{"test"}}
-> const char*
根据 [over.ics.list]/10 and [over.ics.list]/11 不存在:
Otherwise, if the parameter type is not a class:
if the initializer list has one element that is not itself an initializer list...
if the initializer list has no elements...
In all cases other than those enumerated above, no conversion is possible.
要确定{{"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中选择了显式构造函数(从{"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"}}
-> 根据 [over.best.ics]/4:
const B&
不存在
However, if the target is
— the first parameter of a constructor or
— the implicit object parameter of a user-defined conversion function
and the constructor or user-defined conversion function is a candidate by
— [over.match.ctor], when the argument is the temporary in the second step of a class copy-initialization,
— [over.match.copy], [over.match.conv], or [over.match.ref] (in all cases), or
— the second phase of [over.match.list] when the initializer list has exactly one element that is itself an initializer list, and the target is the first parameter of a constructor of class X, and the conversion is to X or reference to cv X,
user-defined conversion sequences are not considered.
确定{{"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&)
,结果也与主要示例相同。