Почему вектор:: push_back и emplace_back вызывают метод value_type:: constructor дважды?
У меня есть этот класс:
class Foo {
public:
Foo() {}
Foo(const Foo&){cout << "constructed by lvalue reference." <<endl; }
Foo(Foo&& ) {cout << "constructed by rvalue reference." << endl; }
};
тогда я вставляю в вектор:
Foo foo{};
vf.push_back(foo);
Выход удивляет:
constructed by lvalue reference.
constructed by lvalue reference.
Я предполагаю, что он был скопирован при передаче параметров, поэтому я попробовал:
vf.push_back(move(foo));
и
vf.push_back(forward<Foo>(foo));
Вывод немного отличается из-за семантики перемещения, но по-прежнему вызывает конструктор дважды:
constructed by rvalue reference.
constructed by lvalue reference.
Почему конструкторы дважды вызывались? Насколько это влияет на производительность? Как я могу избежать этого?
Я использую mingw-gcc-4.7.1 в Windows Vistap >
Общий пример:
#include <iostream>
#include <vector>
using namespace std;
class Foo {
public:
Foo() {}
Foo(const Foo&){cout << "constructed by lvalue reference." <<endl; }
Foo(Foo&& ) {cout << "constructed by rvalue reference." << endl; }
};
int main(int argc, char **argv, char** envp)
{
vector<Foo> vf;
cout << "Insert a temporary." << endl;
vf.emplace_back(Foo{});
Foo foo{};
cout << "Insert a variable." << endl;
vf.emplace_back(foo);
return 0;
}
Точный выход:
Insert a temporary.
constructed by rvalue reference.
Insert a variable.
constructed by lvalue reference.
constructed by lvalue reference.
Ответы
Ответ 1
Когда вы вставляете новые элементы в вектор, вектор может выделять больше памяти для соответствия этим объектам. Когда это произойдет, ему нужно скопировать все его элементы в новое место памяти. Это вызовет конструктор копирования. Поэтому, когда вы вставляете элемент, вы получаете конструктор для этого нового элемента и конструктора при копировании предыдущего элемента.
Ответ 2
vector<Foo> vf;
cout << "Insert a temporary." << endl;
vf.emplace_back(Foo{});
Что происходит выше, так это создание временного Foo
.
Это временное значение затем используется для построения Foo
внутри vector
. Так что "построенный по ссылке rvalue" - это то, о чем вы просили.
Если вы хотите просто построить Foo
на месте, попробуйте:
vs.emplace_back();
Далее:
Foo foo{};
cout << "Insert a variable." << endl;
vf.emplace_back(foo);
здесь вы создаете не временную Foo
. Затем вы инструктируете std::vector
построить новый элемент в конце списка.
Интересно, что вы получаете две конструкции по ссылке lvalue. Второй, по-видимому, вызван изменением размера. Почему изменение размера приводит к тому, что вы ссылаетесь на ссылку lvalue вместо ссылки rvalue - это трюк: если ваш конструктор move
не помечен noexcept
, std::vector
возвращается к копии вместо move
!
Здесь - живой пример, иллюстрирующий вышеприведенные принципы:
#include <iostream>
#include <vector>
using namespace std;
class Foo {
public:
Foo() {}
virtual ~Foo() {}
Foo(const Foo&){cout << "constructed by lvalue reference." <<endl; }
Foo(Foo&){cout << "constructed by non-const lvalue reference." <<endl; }
Foo(Foo&& ) noexcept {cout << "constructed by rvalue reference." << endl; }
};
int main(int argc, char **argv, char** envp)
{
vector<Foo> vf;
cout << "Insert a temporary. One move:" << endl;
vf.emplace_back(Foo{});
cout << "Insert a temporary(2). Two moves:" << endl;
vf.emplace_back(Foo{});
cout << "Resize with temporary(3). Two moves:" << endl;
vf.resize(10);
vector<Foo> vf2;
Foo foo{};
cout << "Insert a variable. One copy:" << endl;
vf2.emplace_back(foo);
cout << "Insert a variable(2). One move, one copy:" << endl;
vf2.emplace_back(foo);
cout << "Resize with variable(3). Two moves:" << endl;
vf2.resize(10);
vector<Foo> vf3;
cout << "Insert a nothing. No copy or move:" << endl;
vf3.emplace_back();
cout << "Insert a nothing(2). One move:" << endl;
vf3.emplace_back();
cout << "Resize with nothing(3). Two moves:" << endl;
vf3.resize(10);
}
Ответ 3
Общая реализация std::vector::push_back
выглядит так:
void push_back(value_type _Val)
{ // insert element at end
insert_n(size(), 1, _Val);
}
Как вы можете видеть, входной параметр передается по значению (поэтому он копируется) как в объявлении push_back
, так и в объявлении insert_n
. Следовательно, copy-constructor вызывается дважды.
После очистки вашего синтаксиса:
#include <iostream>
#include <vector>
using namespace std;
class Foo
{
public:
Foo() {}
Foo(const Foo&) {cout << "constructed by lvalue reference." <<endl; }
Foo(Foo&&) {cout << "constructed by rvalue reference." << endl; }
};
int main()
{
vector<Foo> vf;
cout << "Size = " << vf.size() << endl;
cout << "Capacity = " << vf.capacity() << endl;
cout << "Insert a temporary" << endl;
vf.push_back(Foo()); // this is still very discouraged syntax
Foo foo;
cout << "Insert a variable." << endl;
vf.push_back(foo);
return 0;
}
Вы получаете следующий результат:
Size = 0
Capacity = 0
Insert a temporary
constructed by rvalue reference.
Insert a variable.
constructed by rvalue reference.
constructed by lvalue reference.
Press any key to continue . . .
В этом примере я использую стандартную версию std::vector (которая передается по const-ссылке или по ссылке на ссылку). Первоначальный вызов push_back создает емкость 1 (и размер 1). Второй вызов создает новый блок памяти, перемещает первый элемент и копирует второй (вновь добавленный) элемент.
Что касается производительности, вы не собираетесь сильно пострадать от небольших перераспределений. Существует несколько различных моделей общей памяти (один с Visual Studio растет по экспоненте каждый раз, когда он должен расти, чтобы уменьшить потребность в нем в будущем). Если вы знаете, что вы начнете с 100 элементов, вы должны зарезервировать место при создании своего вектора, поэтому распределение происходит только один раз, что также предотвратит необходимость перемещения существующих элементов при вставке новых элементов (из-за того, что вы не будете будет превышать вашу способность несколько раз).