在这个C++代码中技术上发生了什么?

kay*_*ahr 45 c++ c++11

我有一个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;
}
Run Code Online (Sandbox Code Playgroud)

现在,当我运行这个程序(使用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
Run Code Online (Sandbox Code Playgroud)

因此,不只是A编译器的一个实例生成它的五个实例.A被移动两次并被复制两次.我没想到.向量通过引用传递给构造函数,然后复制到类字段中.所以我本来期望单个复制操作甚至只是一个移动操作(因为我希望传递给构造函数的向量只是一个rvalue),而不是两个副本和两个移动.有人可以解释一下这段代码究竟发生了什么?它在哪里以及为什么要创建所有这些副本A

T.C*_*.C. 42

以相反的顺序进行构造函数调用可能会有所帮助.

B b({ A() });
Run Code Online (Sandbox Code Playgroud)

要构造一个B,编译器必须调用B的构造函数来获取const vector<A>&.反过来,该构造函数必须复制该向量,包括其所有元素.那是你看到的第二个复制电话.

要构造要传递给构造函数的临时向量B,编译器必须调用initializer_list构造函数std::vector.反过来,该构造函数必须复制initializer_list*中包含的内容.这是你看到的第一个拷贝构造函数调用.

该标准规定了如何initializer_list在§8.5.4[dcl.init.list]/p5中构造对象:

类型的对象std::initializer_list<E>是从初始化列表构造的,就好像实现分配了一个类型为const E**的N个元素的数组,其中N是初始化列表中的元素数.使用初始化列表的相应元素对该数组的每个元素进行复制初始化,并std::initializer_list<E>构造该 对象以引用该数组.

从同一类型的对象复制初始化使用重载决策来选择要使用的构造函数(§8.5[dcl.init]/p17),因此对于相同类型的右值,它将调用移动构造函数(如果有的话)可用.因此,为了构造initializer_list<A>来自支撑的初始化列表,编译器将首先const A通过从A构造的临时构造移动来构造一个数组A(),从而引起移动构造函数调用,然后构造该initializer_list对象以引用该数组.

不过,我无法弄清楚g ++中的其他举动来自哪里.initializer_lists通常只是一对指针,标准要求复制一个指针不会复制底层元素.g ++似乎在从临时创建时调用移动构造函数两次initializer_list.它甚至在构造一个左值时调用move构造函数initializer_list.

我最好的猜测是,它正在逐字地实施标准的非规范性示例.该标准提供以下示例:

struct X {
    X(std::initializer_list<double> v);
};

X x{ 1,2,3 };
Run Code Online (Sandbox Code Playgroud)

初始化将以大致相当于此的方式实现:**

const double __a[3] = {double{1}, double{2}, double{3}};
X x(std::initializer_list<double>(__a, __a+3));
Run Code Online (Sandbox Code Playgroud)

假设实现可以使用一对指针构造initializer_list对象.

因此,如果你从字面上看这个例子,initializer_list我们案例中的底层数组将被构造为:

const A __a[1] = { A{A()} };
Run Code Online (Sandbox Code Playgroud)

这会导致两个移动构造函数调用,因为它构造一个临时的A,A从第一个临时复制初始化第二个临时,然后从第二个临时复制初始化数组成员.然而,该标准的规范性文本清楚地表明,应该只有一个拷贝初始化,而不是两个,所以这似乎是一个bug.

最后,第一个A::A直接来自A().

关于析构函数调用的讨论并不多.构造期间创建的所有临时(无论数量)b都将在语句结束时以与构造相反的顺序被破坏,并且当超出范围时,A存储的临时值将被破坏.bb


* 标准库容器initializer_list构造函数被定义为等效于调用带有list.begin()和的两个迭代器的构造函数list.end().这些成员函数返回a const T*,因此无法移动.在C++ 14中,制作了后备阵列const,因此更加清晰,您无法移动它或以其他方式更改它.

** 这个答案最初引用了N3337(C++ 11标准加上一些小的编辑修改),其中数组的元素类型E而不是const E和示例中的数组类型double.在C++ 14中,底层数组是CWG 1418const的结果.


Min*_*ine 5

尝试将代码拆分一点以更好地理解行为:

int main(void) {
    cout<<"Begin"<<endl;
    vector<A> va({A()});

    cout<<"After va;"<<endl;
    B b(va);

    cout<<"After b;"<<endl;
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

输出类似(注意-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
Run Code Online (Sandbox Code Playgroud)