简短版:
在直接初始化中,例如 B b({a1, a2})
,花括号包围的初始化列表 {a1, a2}
被视为构造函数 B
的一个参数。这个参数 {a1, a2}
将被用于初始化构造函数的第一个参数。 B
包含一个隐式声明的构造函数 B(B const&)
。通过创建一个临时的 B
,可以从 {a1, a2}
初始化引用 B const&
。这个临时对象包含一个 A
子对象,并且这个子对象最终将通过 B(B const&)
拷贝构造函数被复制到 b.m_a
中。
相比之下:
void foo(B const& b0);
foo({a1, a2}); // one argument, creates a temporary `B`
对于这种形式的初始化 B b{a1, a2}
,B b(a1, a2)
和 B b = {a1, a2}
,除非存在可行的 std::initializer_list
构造函数,否则我们将不会看到任何复制,因为这些情况将 a1
和 a2
视为(分开的)参数。
B(const A& a) : m_a(a) {}
B(const A& a1, const A& a2) {}
B(B const&) = default;
B(B&&) = default;
#3
由于缺乏隐式提供特殊移动函数的支持,不会出现在VS2013中。 #0在OP的程序中未使用。
初始化B b({a1,a2})
必须选择其中一个构造函数。我们只提供一个参数{a1,a2}
,因此#1不可行。#0也不可行,因为A
无法从两个参数构造。#2和#3仍然可用(#3在VS2013中不存在)。
现在,重载决议尝试从{a1,a2}
初始化B const&
或B&&
。将创建一个临时的B
并绑定到此引用。如果#3存在,则重载决议将优先选择#3而不是#2。
创建临时变量再次查看上面显示的四个构造函数,但现在我们具有两个参数a1
和a2
(或initializer_list
,但这在此处与本题无关)。#1是唯一可行的重载,并且通过B(const A& a1,const A& a2)
创建了临时变量。
因此,我们最终实际上会得到B b(B {a1,a2})
。从临时变量B{a1,a2}
到b
的复制(或移动)可以被省略(复制省略)。这就是为什么g++和clang++不会调用B
或A
的复制构造函数或移动构造函数。
在这里,VS2013似乎无法省略复制构造。它也不能移动构造,因为它不能隐式提供#3(VS2015将修复该问题)。因此,VS2013调用B(B const&)
,它将B {a1,a2}.m_a
复制到b.m_a
。这将调用A
的复制构造函数。
如果#3存在且移动没有被省略,则将调用隐式声明的移动构造函数#3。由于A
具有显式声明的复制构造函数,因此不会为A
隐式声明移动构造函数。这也导致从B{a1,a2}.m_a
到b.m_a
的复制构造,但通过B
的移动构造函数实现。
在VS2013中,如果我们手动向A
和B
添加移动构造函数,我们将注意到A
将被移动而不是复制:
#include <iostream>
#include <utility>
struct A
{
A() = default;
A(int i) : m_i(i) {}
A(const A& a)
{
std::cout << "copy A " << m_i << std::endl;
}
A(A&& a)
{
std::cout << "move A " << m_i << std::endl;
}
int m_i = 0;
};
struct B
{
B(const A& a1, const A& a2) {}
B(B const&) = default;
B(B&& b) : m_a(std::move(b.m_a)) {}
A m_a;
};
跟踪每个构造函数通常更容易理解这样的程序。使用 MSVC 特有的 __FUNCSIG__
(g++/clang++ 可以使用 __PRETTY_FUNCTION__
):
#include <iostream>
#define PRINT_FUNCSIG() { std::cout << __FUNCSIG__ << "\n"; }
struct A
{
A() PRINT_FUNCSIG()
A(int i) : m_i(i) PRINT_FUNCSIG()
A(const A& a) : m_i(a.m_i) PRINT_FUNCSIG()
int m_i = 0;
};
struct B
{
B(const A& a1, const A& a2) PRINT_FUNCSIG()
B(B const& b) : m_a(b.m_a) PRINT_FUNCSIG()
A m_a;
};
int main()
{
A a1{1}, a2{2};
B b({ a1, a2 });
return 0;
}
这将打印(不包括注释):
__thiscall A::A(int) // a1{1}
__thiscall A::A(int) // a2{2}
__thiscall A::A(void) // B{a1, a2}.m_a,默认构造
__thiscall B::B(const struct A &,const struct A &) // B{a1, a2}
__thiscall A::A(const struct A &) // b.m_a(B{a1, a2}.m_a)
__thiscall B::B(const struct B &) // b(B{a1, a2})
额外的信息:
- VS2015和VS2013确实省略了
B b(B{a1,a2});
的复制构造,但没有省略原始的B b({a1,a2})
。
B b({a1, a2});
几乎等同于B b(B{a1, a2});
。复制可能发生在从B{a1, a2}.m_a
临时对象到b.m_a
的过程中。 - dyp