这个C ++代码在技术上会发生什么?
我有一个B
类,它包含一个A
类的向量。 我想通过构造函数初始化这个向量。 A类输出一些debugging信息,所以我可以看到它的构造,破坏,复制或移动。
#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
移动两次,并复制两次。 我没想到。 该vector通过引用传递给构造函数,然后复制到类字段中。 所以我会期望一个复制操作,甚至只是一个移动操作(因为我希望我传递给构造函数的向量只是一个右值),而不是两个副本和两个移动。 有人可以请解释这个代码究竟发生了什么? 为什么它会创build所有这些A
副本?
以相反的顺序通过构造函数调用可能会有所帮助。
B b({ A() });
要构造一个B
,编译器必须调用B的构造函数,该构造函数需要一个const vector<A>&
。 该构造函数反过来必须复制vector,包括其所有元素。 这是你看到的第二个副本。
要构造要传递给B
的构造函数的临时向量,编译器必须调用std::vector
的initializer_list
构造函数。 这个构造函数反过来必须复制initializer_list
*中包含的内容。 这是你看到的第一个复制构造函数。
该标准指定了如何在§8.5.4[dcl.init.list] / p5中构造initializer_list
对象:
std::initializer_list<E>
types的对象是从初始化程序列表构造的,就好像实现分配了一个types为const E
**的N个元素的数组,其中N是初始化程序列表中元素的数目。 该数组的每个元素都使用初始化列表的相应元素进行了复制初始化,std::initializer_list<E>
对象被构造为引用该数组。
从相同types的对象中复制初始化使用重载parsing来select要使用的构造函数(§8.5[dcl.init] / p17),所以如果有一个相同types的右值,它将调用移动构造函数可用。 因此,为了从支撑初始化列表构造initializer_list<A>
,编译器将首先通过从由A()
构造的临时A
移动来构造一个const A
的数组,引发一个移动构造函数调用,然后构造initializer_list
对象来引用该数组。
不过,我无法弄清楚g ++中的其他动作是从哪里来的。 initializer_list
通常只不过是一对指针,而复制一个指针的标准命令不会复制底层元素。 当从临时创build一个initializer_list
时,g ++似乎调用两次移动构造 initializer_list
。 它甚至在从左值构造一个initializer_list
时调用移动构造函数。
我最好的猜测是,它是从字面上实现标准的非规范性示例。 该标准提供了以下示例:
struct X { X(std::initializer_list<double> v); }; X x{ 1,2,3 };
初始化将以大致等同于此的方式实现: **
const double __a[3] = {double{1}, double{2}, double{3}}; X x(std::initializer_list<double>(__a, __a+3));
假设实现可以用一对指针构造一个initializer_list对象。
所以,如果你直接使用这个例子,那么在我们的例子中, initializer_list
下面的数组将被构造为如下forms:
const A __a[1] = { A{A()} };
它会产生两个移动构造函数调用,因为它会构造一个临时的A
,拷贝 – 从第一个临时A
初始化第二个临时A
,然后从第二个临时对数组成员进行复制 – 初始化。 然而,标准的规范性文本清楚地表明,应该只有一个复制初始化,而不是两个,所以这看起来像一个错误。
最后,第一个A::A
直接来自A()
。
关于析构函数调用没有太多讨论。 在施工过程中产生的所有临时施工(无论数量)都会在施工结束时按照相反的顺序进行破坏,当施工b
超出施工范围时,存储在施工中的A
将会被破坏。
* 标准库容器的initializer_list
构造函数被定义为等同于调用带有list.begin()
和list.end()
两个迭代器的构造函数。 那些成员函数返回一个const T*
,所以它不能被移动。 在C ++ 14中,支持数组是const
,所以更加清楚的是你不可能移动它或者改变它。
** 这个答案最初引用了N3337(C ++ 11标准加上一些小的编辑性修改),其中数组的元素types是E
而不是const E
,而例子中的数组是double
types的。 在C ++ 14中,作为CWG 1418的结果,基础数组被设置为const
。
尝试分解代码以更好地理解行为:
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
考虑一下:
- 临时
A
被实例化:A()
- 该实例被移动到初始化程序列表中:
A(A&&)
- 初始化器列表被移动到向量ctor,所以它的元素被移动:
A(A&&)
。 编辑 : 正如TC注意到,initializer_list元素不会被移动/复制initializer_list移动/复制。 如他的代码示例所示,似乎在initializer_list初始化期间使用了两个右值ctor调用。 - 向量元素是通过值初始化的,而不是通过移动(为什么?,我不确定):
A(const A&)
编辑: 同样,不是vector,而是初始化器列表 - 你的ctor获取那个时间向量并复制它(注意你的向量初始化),所以元素被复制:
A(const A&)
A::A
构造函数在创build临时对象时执行。
第一个A :: A(A &&)
临时对象被移入初始化列表(也是右值)。
第二个A :: A(A &&)
初始化列表被移入向量的构造函数。
第一个A :: A(A&)
该向量被复制,因为B的构造函数采用左值,并通过右值。
第二个A :: A(A&)
同样,在创buildB的成员variablesva
时复制向量。
A ::〜阿
A ::〜阿
A ::〜阿
A ::〜阿
A ::〜阿
为每个右值和左值调用析构函数(无论何时调用构造函数,复制或移动构造函数,当对象被销毁时执行析构函数)。