我有一个班级B
,其中包含班级A
的向量。我想通过构造函数初始化此向量。类A
输出一些调试信息,以便我可以看到它何时被构造,破坏,复制或移动。
#include <vector>
#include <iostream>
using namespace std;
class A {
public:
A() { cout << "A::A" << endl; }
~A() { cout << "A::~A" << endl; }
A(const A& t) { cout <<"A::A(A&)" << endl; }
A(A&& t) { cout << "A::A(A&&)" << endl; }
};
class B {
public:
vector<A> va;
B(const vector<A>& va) : va(va) {};
};
int main(void) {
B b({ A() });
return 0;
}
现在,当我运行此程序时(使用GCC选项-fno-elide-constructors
编译,因此移动构造函数调用未被优化)我得到以下输出:
A::A
A::A(A&&)
A::A(A&&)
A::A(A&)
A::A(A&)
A::~A
A::~A
A::~A
A::~A
A::~A
因此编译器只生成A
的一个实例而不是一个实例。 A
被移动两次并被复制两次。我没想到。向量通过引用传递给构造函数,然后复制到类字段中。所以我本来期望单个复制操作甚至只是一个移动操作(因为我希望传递给构造函数的向量只是一个rvalue),而不是两个副本和两个移动。有人可以解释一下这段代码究竟发生了什么?它在何处以及为何创建A
的所有副本?
答案 0 :(得分:42)
以相反的顺序进行构造函数调用可能会有所帮助。
B b({ A() });
要构造B
,编译器必须调用带有const vector<A>&
的B构造函数。反过来,该构造函数必须复制该向量,包括其所有元素。那是你看到的第二个复制电话。
要构造要传递给B
构造函数的临时向量,编译器必须调用initializer_list
的{{1}}构造函数。反过来,该构造函数必须复制std::vector
* 中包含的内容。这是你看到的第一个拷贝构造函数。
该标准规定了如何在§8.5.4[dcl.init.list] / p5中构建initializer_list
个对象:
initializer_list
类型的对象是由一个构造的 初始化列表,好像实现分配了一个N数组 类型为std::initializer_list<E>
** 的元素,其中N是元素的数量 初始化列表。该数组的每个元素都使用复制初始化 初始化列表的对应元素,以及 构造const E
对象以引用该数组。
从相同类型的对象复制初始化使用重载决策来选择要使用的构造函数(§8.5[dcl.init] / p17),因此使用相同类型的rvalue将调用移动构造函数如果有的话。因此,要从支撑的初始化列表构造std::initializer_list<E>
,编译器将首先通过从initializer_list<A>
构造的临时const A
移动来构造一个A
的数组,从而导致移动构造函数调用,然后构造A()
对象以引用该数组。
initializer_list
通常只是一对指针,标准要求复制一个指针不会复制底层元素。从临时创建initializer_list
时,g ++似乎call the move constructor twice。从左值构建initializer_list
时,它甚至是calls the move constructor。
我最好的猜测是它正在逐字地实施标准的非规范性示例。该标准提供以下示例:
initializer_list
初始化将以大致相当于的方式实现 这样: **
struct X { X(std::initializer_list<double> v); }; X x{ 1,2,3 };
假设实现可以使用一对指针构造initializer_list对象。
因此,如果你从字面上理解这个例子,我们案例中基于const double __a[3] = {double{1}, double{2}, double{3}};
X x(std::initializer_list<double>(__a, __a+3));
的数组将被构造为:
initializer_list
它会引发两个移动构造函数调用,因为它构造一个临时const A __a[1] = { A{A()} };
,从第一个临时A
复制初始化第二个临时A
,然后从第二个临时复制初始化数组成员。然而,标准的规范性文本清楚地表明,应该只有一个拷贝初始化,而不是两个,所以这似乎是一个bug。
最后,第一个A::A
直接来自A()
。
关于析构函数调用的讨论并不多。在b
构造期间创建的所有临时(无论数量)将在构造的相反顺序在语句末尾被破坏,A
中存储的b
将被破坏当b
超出范围时。
* 标准库容器的initializer_list
构造函数被定义为等效于调用带有list.begin()
和list.end()
的两个迭代器的构造函数。这些成员函数返回const T*
,因此无法移动。在C ++ 14中,后备数组是const
,所以更清楚的是你不可能从它移动或以其他方式改变它。
** 这个答案最初引用了N3337(C ++ 11标准加上一些小的编辑修改),其中数组的元素类型为E
,而不是{ {1}}示例中的数组为const E
类型。在C ++ 14中,作为CWG 1418的结果,底层数组被double
。
答案 1 :(得分:5)
尝试稍微拆分代码以更好地理解行为:
int main(void) {
cout<<"Begin"<<endl;
vector<A> va({A()});
cout<<"After va;"<<endl;
B b(va);
cout<<"After b;"<<endl;
return 0;
}
输出类似(注意使用了-fno-elide-constructors
)
Begin
A::A <-- temp A()
A::A(A&&) <-- moved to initializer_list
A::A(A&&) <-- no idea, but as @Manu343726, it's moved to vector's ctor
A::A(A&) <-- copied to vector's element
A::~A
A::~A
A::~A
After va;
A::A(A&) <-- copied to B's va
After b;
A::~A
A::~A
答案 2 :(得分:3)
考虑一下:
A
已获得实例:A()
A(A&&)
A(A&&)
。
编辑:作为T.C.注意,initializer_list元素不会被移动/复制以进行initializer_list移动/复制。正如他的代码示例所示,似乎在initializer_list初始化期间使用了两个rvalue ctor调用。 A(const A&)
编辑: 同样,不是向量,但初始化列表 A(const A&)
答案 3 :(得分:0)
A::A
在创建临时对象时执行构造函数。
首先是A :: A(A&amp;&amp;)
临时对象被移动到初始化列表(也是右值)。
秒A :: A(A&amp;&amp;)
初始化列表被移动到vector的构造函数中。
首先是A :: A(A&amp;)
复制向量是因为B的构造函数接受了左值,并传递了右值。
秒A :: A(A&amp;)
同样,在创建B的成员变量va
时会复制向量。
A ::〜甲
A ::〜阿
A ::〜阿
A ::〜阿
A :: ~A
为每个rvalue和lvalue调用析构函数(每当调用构造函数,复制或移动构造函数时,在对象被销毁时执行析构函数。)