Что технически происходит в этом коде 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

Учти это:

  1. Временный A например: A()
  2. Этот экземпляр перемещен в список инициализатора: A(A&&)
  3. Список инициализаторов перемещается в вектор ctor, поэтому его элементы перемещаются: A(A&&), РЕДАКТИРОВАТЬ: Как заметил TC, элементы initializer_list не перемещаются / копируются для перемещения / копирования initializer_list. Как показывает его пример кода, кажется, что два вызова rvalue ctor используются во время инициализации initializer_list.
  4. Элемент вектора инициализируется значением, а не перемещением (Почему? Я не уверен): A(const A&) РЕДАКТИРОВАТЬ: опять же, не вектор, а список инициализатора
  5. Ваш 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 (всякий раз, когда вызывается конструктор, копировать или перемещать конструкторы, деструктор выполняется, когда объекты уничтожаются).

Другие вопросы по тегам