Подтвердить что ты не робот

Что происходит технически в этом коде на С++?

У меня есть класс 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 перемещается два раза и копируется два раза. Я этого не ожидал. Вектор передается ссылкой на конструктор и затем копируется в поле класса. Поэтому я ожидал бы одну операцию копирования или даже операцию перемещения (потому что я надеялся, что вектор, который я передаю конструктору, - это просто rvalue), а не две копии и два хода. Может кто-нибудь объяснить, что именно происходит в этом коде? Где и почему он создает все эти копии A?

4b9b3361

Ответ 1

Может быть полезно пройти через вызовы конструктора в обратном порядке.

B b({ A() });

Чтобы построить B, компилятор должен вызвать конструктор B, который принимает const vector<A>&. Этот конструктор, в свою очередь, должен сделать копию вектора, включая все его элементы. Вы увидите, что второй экземпляр ctor вы видите.

Чтобы создать временный вектор, который нужно передать конструктору B, компилятор должен вызвать конструктор initializer_list std::vector. Этот конструктор, в свою очередь, должен сделать копию того, что содержится в initializer_list *. Вы увидите, что первый конструктор копирования вы видите.

В стандарте указано, как объекты initializer_list создаются в § 8.5.5 [dcl.init.list]/p5:

Объект типа std::initializer_list<E> строится из список инициализаторов, как если бы реализация выделила массив из N элементы типа const E ** где N - количество элементов в список инициализаторов. Каждый элемент этого массива инициализируется с помощью соответствующий элемент списка инициализатора и std::initializer_list<E> объект построен для обращения к этому массиву.

Копирование-инициализация объекта из одного и того же типа использует разрешение перегрузки для выбора используемого конструктора (§8.5 [dcl.init]/p17), поэтому с rvalue того же типа он будет ссылаться на конструктор перемещения если таковая имеется. Таким образом, чтобы построить 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 погаснет объема.


* Конструкторы стандартных контейнеров библиотеки initializer_list определяются как эквивалентные вызову конструктора с двумя итераторами с list.begin() и list.end(). Эти функции-члены возвращают const T*, поэтому его нельзя перенести. В С++ 14 массив подстановки выполнен const, поэтому он еще более ясен, что вы не можете его переместить или иначе изменить.

** Этот ответ изначально цитировал N3337 (стандарт С++ 11 плюс некоторые незначительные редакционные изменения), который имеет массив, имеющий элементы типа E, а не const E и массив в примере имеет тип double. В С++ 14 базовый массив был сделан const в результате CWG 1418.

Ответ 2

Попробуйте немного разбить код, чтобы лучше понять поведение:

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 moved to vector ctor
A::A(A&)    <-- copied to vector element
A::~A
A::~A
A::~A
After va;
A::A(A&)    <-- copied to B va
After b;
A::~A
A::~A

Ответ 3

Рассмотрим это:

  • Временная A установлена: A()
  • Этот экземпляр перемещается в список инициализаторов: A(A&&)
  • Список инициализаторов перемещается в вектор ctor, поэтому его элементы перемещаются: A(A&&). EDIT: как T.C. заметили, что элементы initializer_list не перемещаются/не копируются для перемещения/копирования initializer_list. Как показывает его пример кода, кажется, что во время инициализации initializer_list используются два вызова rvalue ctor.
  • Элемент vector инициализируется значением, а не move (почему?, я не уверен): A(const A&) EDIT: Опять же, это не вектор, а список инициализаторов
  • Ваш ctor получает этот временный вектор и копирует его (обратите внимание на свой векторный инициализатор), поэтому элементы скопированы: A(const A&)

Ответ 4

A::A

Конструктор выполняется, когда создается временный объект.

сначала A:: A (A & &)

Временный объект перемещается в список инициализации (который также является rvalue).

второй A:: A (A & &)

Список инициализации перемещается в векторный конструктор.

сначала A:: A (A &)

Вектор копируется, потому что конструктор B принимает значение lvalue и передается значение r.

второй A:: A (A &)

Опять же, вектор копируется при создании переменной члена B va.

A:: ~ A
A:: ~ A
A:: ~ A
A:: ~ A
A:: ~ A

Деструктор вызывается для каждого значения rvalue и lvalue (всякий раз, когда вызываются конструкторы, копируют или перемещают конструкторы, деструктор выполняется, когда объекты уничтожаются).