这个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::vectorinitializer_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 ,而例子中的数组是doubletypes的。 在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 

考虑一下:

  1. 临时A被实例化: A()
  2. 该实例被移动到初始化程序列表中: A(A&&)
  3. 初始化器列表被移动到向量ctor,所以它的元素被移动: A(A&&)编辑正如TC注意到,initializer_list元素不会被移动/复制initializer_list移动​​/复制。 如他的代码示例所示,似乎在initializer_list初始化期间使用了两个右值ctor调用。
  4. 向量元素是通过值初始化的,而不是通过移动(为什么?,我不确定): A(const A&) 编辑: 同样,不是vector,而是初始化器列表
  5. 你的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 ::〜阿

为每个右值和左值调用析构函数(无论何时调用构造函数,复制或移动构造函数,当对象被销毁时执行析构函数)。