反向查看构造函数调用可能会有帮助。
B b({ A() });
为构造一个
B
,编译器必须调用接受
const vector<A>&
的 B 构造函数。该构造函数反过来必须复制向量,包括其所有元素。这就是你看到的第二个复制构造函数调用。
为构造要传递给
B
构造函数的临时向量,编译器必须调用
std::vector
的
initializer_list
构造函数。该构造函数反过来必须复制
initializer_list
中所包含的内容。这就是你看到的第一个复制构造函数调用。
标准在 §8.5.4 [dcl.init.list]/p5 中规定了如何构造
initializer_list
对象:
类型为 std::initializer_list<E>
的对象从初始化列表中构造,就好像实现分配了 N 个类型为 const E
的元素的数组,其中 N 是初始化列表中的元素数。该数组的每个元素都使用相应的初始化列表中的元素进行复制初始化,并构造 std::initializer_list<E>
对象以引用该数组。
从相同类型的某个东西复制初始化对象会使用重载决议来选择要使用的构造函数(§8.5 [dcl.init]/p17),因此,对于相同类型的 rvalue,如果有可用的移动构造函数,它将调用该移动构造函数。因此,为了从花括号初始化列表构造
initializer_list<A>
,编译器将首先构造一个由临时构造的
A
移动而来的一个
const A
数组,导致移动构造函数调用,然后构造
initializer_list
对象以引用该数组。
不过,我无法确定 g++ 中的另一个移动构造函数是从哪里来的。
initializer_list
通常只是一对指针,标准规定复制它不会复制底层元素。当从临时对象创建
initializer_list
时,g++ 似乎会
调用两次移动构造函数。甚至在从 lvalue 构造
initializer_list
时也会
调用移动构造函数。
我最好的猜测是它按照标准的非规范性示例文字实现。标准提供了以下示例:
struct X {
X(std::initializer_list<double> v);
};
X x{ 1,2,3 };
The initialization will be implemented in a way roughly equivalent to
this:**
const double __a[3] = {double{1}, double{2}, double{3}};
X x(std::initializer_list<double>(__a, __a+3));
assuming that the implementation can construct an initializer_list object with a pair of pointers.
如果您字面上理解这个例子,我们案例中的initializer_list
下层的数组将被构建为:
const A __a[1] = { A{A()} };
由于它创建了一个临时的A
,从第一个临时对象复制初始化了第二个临时对象,然后从第二个临时对象复制初始化了数组成员,因此会产生两次移动构造函数调用。但是标准的规范文本明确指出应该只有一次复制初始化,而不是两次,所以这似乎是一个错误。
最后,第一个A::A
直接来自A()
。
关于析构函数调用,没有太多讨论。在构建b
期间创建的所有临时对象(无论数量如何)都将在语句结尾以构建顺序相反的顺序被销毁,并且存储在b
中的A
将在b
超出作用域时被销毁。
* 标准库容器的initializer_list
构造函数被定义为等价于使用list.begin()
和list.end()
的构造函数,这些成员函数返回一个const T*
,因此无法从中移动。在C ++14中,支持数组的底层数组变为const
,因此更清楚地表明您无法从中移动或以其他方式更改它。
** 此答案最初引用了N3337(C ++11标准加上一些小的编辑更改),该数组具有类型E
的元素而不是const E
,并且示例中的数组为double
类型。在C ++14中,由于CWG 1418的结果,基础数组被设置为const
。
B
的构造函数中移动向量,你需要提供一个重载函数,接受vector<A> &&
或者通过值传递向量并使用: va(std::move(va))
。如果你使用const引用,它总是会进行复制。 - T.C.initializer_list
复制,因为没有办法从其元素中移动。您将不得不手动填充向量(使用push_back
的rvalue引用重载或简单地使用emplace_back
),然后将该向量std::move
到B
的构造函数中。 - T.C.