Что технически происходит в этом коде C++?
У меня есть класс 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
перемещается два раза и копируется два раза. Я этого не ожидал. Вектор передается по ссылке на конструктор, а затем копируется в поле класса. Таким образом, я ожидал бы одну операцию копирования или даже просто операцию перемещения (потому что я надеялся, что вектор, который я передаю конструктору, является просто значением), а не две копии и два перемещения. Может кто-нибудь объяснить, что именно происходит в этом коде? Где и почему он создает все эти копии A
?
4 ответа
Может быть полезно пройти вызовы конструктора в обратном порядке.
B b({ A() });
Чтобы построить B
компилятор должен вызвать конструктор B, который принимает const vector<A>&
, Этот конструктор, в свою очередь, должен сделать копию вектора, включая все его элементы. Это вторая копия звонка ctor, которую вы видите.
Построить временный вектор для передачи B
конструктор, компилятор должен вызывать initializer_list
конструктор std::vector
, Этот конструктор, в свою очередь, должен сделать копию того, что содержится в initializer_list
* Это первый вызов конструктора копирования, который вы видите.
Стандарт определяет, как initializer_list
объекты создаются в §8.5.4 [dcl.init.list]/p5:
Объект типа
std::initializer_list<E>
построен из списка инициализаторов, как если бы реализация выделяла массив из N элементов типаconst E
**, где N - количество элементов в списке инициализатора. Каждый элемент этого массива инициализируется копией с соответствующим элементом списка инициализатора, иstd::initializer_list<E>
Объект создан для обращения к этому массиву.
Инициализация копирования объекта из чего-то того же типа использует разрешение перегрузки для выбора используемого конструктора (§8.5 [dcl.init]/p17), поэтому при наличии значения того же типа он вызовет конструктор перемещения, если он имеется в наличии. Таким образом, чтобы построить initializer_list<A>
из списка инициализаторов в скобках компилятор сначала создаст массив из const A
переходя от временного A
построен A()
, вызывая вызов конструктора перемещения, а затем построить initializer_list
объект для ссылки на этот массив.
Однако я не могу понять, откуда взялся другой ход в g++. initializer_list
Обычно это не что иное, как пара указателей, и стандартные указания, что копирование одного не копирует базовые элементы. G ++, кажется, вызывает конструктор перемещения дважды при создании initializer_list
из временного. Он даже вызывает конструктор перемещения при создании initializer_list
от lvalue.
Мое лучшее предположение, что он буквально реализует ненормативный пример стандарта. Стандарт предоставляет следующий пример:
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
в нашем случае будет построен как бы:
const A __a[1] = { A{A()} };
который влечет за собой два вызова конструктора перемещения, потому что он создает временный A
скопировать-инициализировать второй временный A
от первого, затем копирует-инициализирует член массива из второго временного. Нормативный текст стандарта, однако, ясно показывает, что должна быть только одна копия-инициализация, а не две, так что это похоже на ошибку.
Наконец, первый A::A
приходит прямо из A()
,
Там не так много, чтобы обсудить о вызовах деструкторов. Все временные (независимо от количества), созданные во время строительства b
будет уничтожен в конце оператора в обратном порядке построения, а тот A
Хранится в b
будет разрушен, когда b
выходит за рамки.
* The initializer_list
Конструкторы стандартных контейнеров библиотеки определены как эквивалентные вызову конструктора, принимающего два итератора с list.begin()
а также list.end()
, Эти функции-члены возвращают const T*
таким образом, это не может быть перемещено из. В C++14 создан резервный массив const
так что еще яснее, что вы не можете от него отказаться или каким-либо другим способом изменить его.
** Этот ответ первоначально процитирован N3337 (стандарт C++11 плюс некоторые незначительные редакционные изменения), в котором есть массив, имеющий элементы типа E
скорее, чем const E
и массив в примере имеет тип double
, В C++14 базовый массив был сделан const
в результате CWG 1418.
Попробуйте немного разбить код, чтобы лучше понять поведение:
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. Как показывает его пример кода, кажется, что два вызова rvalue ctor используются во время инициализации initializer_list. - Элемент вектора инициализируется значением, а не перемещением (Почему? Я не уверен):
A(const A&)
РЕДАКТИРОВАТЬ: опять же, не вектор, а список инициализатора - Ваш ctor получает этот временной вектор и копирует его (обратите внимание на ваш инициализатор вектора), поэтому элементы копируются:
A(const A&)
A::A
Конструктор выполняется, когда создается временный объект.
первый A::A(A&&)
Временный объект перемещается в список инициализации (который также является rvalue).
второй A::A(A&&)
Список инициализации перемещается в конструктор вектора.
первый A::A(A&)
Вектор копируется, потому что конструктор B принимает значение l, а значение r передается.
второй A::A(A&)
Опять же, вектор копируется при создании переменной члена B va
,
A:: ~ A
A:: ~ A
A:: ~ A
A:: ~ A
A:: ~ A
Деструктор вызывается для каждого rvalue и lvalue (всякий раз, когда вызывается конструктор, копировать или перемещать конструкторы, деструктор выполняется, когда объекты уничтожаются).