为什么vector :: push_back和emplace_back两次调用value_type :: constructor?

时间:2022-05-03 15:32:48

I have this class:

我有这堂课:

class Foo {
public:
    Foo() {}
    Foo(const Foo&){cout << "constructed by lvalue reference." <<endl; }
    Foo(Foo&& ) {cout << "constructed by rvalue reference." << endl; }
};

then I insert into a vector:

然后我插入一个向量:

Foo foo{};
vf.push_back(foo);

The output is surprising:

输出令人惊讶:

constructed by lvalue reference.
constructed by lvalue reference.

I assume it got copied when passing parameters, so I tried:

我假设它在传递参数时被复制了,所以我尝试了:

vf.push_back(move(foo));

and

vf.push_back(forward<Foo>(foo));

The output are slightly different due to move semantics but still calling constructor twice:

由于移动语义但仍然调用构造函数两次,输出略有不同:

constructed by rvalue reference.
constructed by lvalue reference.

Why the constructors got called twice? How much performance does it impact? How can I avoid this?

为什么构造函数被调用两次?它会影响多少性能?我怎么能避免这个?


I am using mingw-gcc-4.7.1 on Windows Vista

我在Windows Vista上使用mingw-gcc-4.7.1

Total example:

总例子:

#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;
}

Exact output:

确切输出:

Insert a temporary.
constructed by rvalue reference.
Insert a variable.
constructed by lvalue reference.
constructed by lvalue reference.

3 个解决方案

#1


14  

When you insert new items in a vector the vector may have to allocate more memory to fit those objects. When that happens it needs to copy all it's elements to the new memory location. That will invoke the copy constructor. So when you insert your element you're getting the constructor for that new element and the constructor when copying the previous element.

在向量中插入新项时,向量可能必须分配更多内存以适合这些对象。当发生这种情况时,它需要将所有元素复制到新的内存位置。那将调用复制构造函数。因此,当您插入元素时,您将获得该元素的构造函数以及复制前一个元素时的构造函数。

#2


6  

  vector<Foo> vf;
  cout << "Insert a temporary." << endl;
  vf.emplace_back(Foo{});

What happens above is that a temporary Foo is created.

上面发生的是创建一个临时Foo。

This temporary is then used to construct a Foo within the vector. So a "constructed by rvalue reference" is what you asked for.

然后使用该临时值在向量内构造Foo。因此,“由右值参考构建”是您所要求的。

If you wish to simply construct the Foo in place, try:

如果您希望简单地构建Foo,请尝试:

  vs.emplace_back();

Next:

下一个:

  Foo foo{};
  cout << "Insert a variable." << endl;
  vf.emplace_back(foo);

here you construct a non-temporary foo. You then instruct the std::vector to construct a new element at the end of the list.

在这里你构建一个非临时的foo。然后,您指示std :: vector在列表末尾构造一个新元素。

The interesting thing is that you get two construct by lvalue reference. The second seems to be caused by the resize. Why the resize causes you to be construced by lvalue reference, instead of rvalue reference, is a trick: if your move constructor is not marked noexcept, std::vector falls back on copy instead of move!

有趣的是,你通过左值引用得到两个构造。第二个似乎是由调整大小引起的。为什么调整大小会导致你被左值引用而不是右值引用构造,这是一个技巧:如果你的移动构造函数没有标记为noexcept,std :: vector会重新复制而不是移动!

Here is a live example illustrating the above principles:

这是一个说明上述原则的实例:

#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


-3  

A common implementation of std::vector::push_back looks like this:

std :: vector :: push_back的常见实现如下所示:

void push_back(value_type _Val)
{   // insert element at end
    insert_n(size(), 1, _Val);
}

As you can see, the input parameter is passed-by-value (so it is copied) in both the push_back declaration and the insert_n declaration. Hence, the copy-constructor is called twice.

如您所见,输入参数在push_back声明和insert_n声明中都是按值传递(因此它被复制)。因此,复制构造函数被调用两次。

After cleaning up your syntax:

清理完语法后:

#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;
}

You get the following output:

您将获得以下输出:

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 . . .

In this example, I'm using a standard version of std::vector (which passes by const-reference or by reference-to-reference). The initial push_back call creates a capacity of 1 (and a size of 1). The 2nd call creates a new block of memory, moves the first item, and copies the second (the newly added) item.

在这个例子中,我使用标准版本的std :: vector(通过const-reference或引用引用)。初始push_back调用创建容量为1(大小为1)。第二个调用创建一个新的内存块,移动第一个项目,并复制第二个(新添加的)项目。

In terms of performance, you aren't going to take a big hit for small reallocations. There are a few different common memory models used (the one with Visual Studio grows your capacity exponentially each time it has to grow to decrease the need for it in the future). If you know you will be starting with 100 elements, you should reserve space when you create your vector so allocation only happens once, which will also prevent the need to move existing elements when inserting new elements (due to the fact that you won't be exceeding your capacity multiple times).

在性能方面,您不会为小型重新分配而受到重创。使用了一些不同的常见内存模型(Visual Studio每次都需要以指数方式增加容量,以便在将来减少对它的需求)。如果你知道你将从100个元素开始,你应该在创建向量时保留空间,这样分配只发生一次,这也可以防止在插入新元素时移动现有元素的需要(因为你不会超过你的容量多次)。

#1


14  

When you insert new items in a vector the vector may have to allocate more memory to fit those objects. When that happens it needs to copy all it's elements to the new memory location. That will invoke the copy constructor. So when you insert your element you're getting the constructor for that new element and the constructor when copying the previous element.

在向量中插入新项时,向量可能必须分配更多内存以适合这些对象。当发生这种情况时,它需要将所有元素复制到新的内存位置。那将调用复制构造函数。因此,当您插入元素时,您将获得该元素的构造函数以及复制前一个元素时的构造函数。

#2


6  

  vector<Foo> vf;
  cout << "Insert a temporary." << endl;
  vf.emplace_back(Foo{});

What happens above is that a temporary Foo is created.

上面发生的是创建一个临时Foo。

This temporary is then used to construct a Foo within the vector. So a "constructed by rvalue reference" is what you asked for.

然后使用该临时值在向量内构造Foo。因此,“由右值参考构建”是您所要求的。

If you wish to simply construct the Foo in place, try:

如果您希望简单地构建Foo,请尝试:

  vs.emplace_back();

Next:

下一个:

  Foo foo{};
  cout << "Insert a variable." << endl;
  vf.emplace_back(foo);

here you construct a non-temporary foo. You then instruct the std::vector to construct a new element at the end of the list.

在这里你构建一个非临时的foo。然后,您指示std :: vector在列表末尾构造一个新元素。

The interesting thing is that you get two construct by lvalue reference. The second seems to be caused by the resize. Why the resize causes you to be construced by lvalue reference, instead of rvalue reference, is a trick: if your move constructor is not marked noexcept, std::vector falls back on copy instead of move!

有趣的是,你通过左值引用得到两个构造。第二个似乎是由调整大小引起的。为什么调整大小会导致你被左值引用而不是右值引用构造,这是一个技巧:如果你的移动构造函数没有标记为noexcept,std :: vector会重新复制而不是移动!

Here is a live example illustrating the above principles:

这是一个说明上述原则的实例:

#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


-3  

A common implementation of std::vector::push_back looks like this:

std :: vector :: push_back的常见实现如下所示:

void push_back(value_type _Val)
{   // insert element at end
    insert_n(size(), 1, _Val);
}

As you can see, the input parameter is passed-by-value (so it is copied) in both the push_back declaration and the insert_n declaration. Hence, the copy-constructor is called twice.

如您所见,输入参数在push_back声明和insert_n声明中都是按值传递(因此它被复制)。因此,复制构造函数被调用两次。

After cleaning up your syntax:

清理完语法后:

#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;
}

You get the following output:

您将获得以下输出:

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 . . .

In this example, I'm using a standard version of std::vector (which passes by const-reference or by reference-to-reference). The initial push_back call creates a capacity of 1 (and a size of 1). The 2nd call creates a new block of memory, moves the first item, and copies the second (the newly added) item.

在这个例子中,我使用标准版本的std :: vector(通过const-reference或引用引用)。初始push_back调用创建容量为1(大小为1)。第二个调用创建一个新的内存块,移动第一个项目,并复制第二个(新添加的)项目。

In terms of performance, you aren't going to take a big hit for small reallocations. There are a few different common memory models used (the one with Visual Studio grows your capacity exponentially each time it has to grow to decrease the need for it in the future). If you know you will be starting with 100 elements, you should reserve space when you create your vector so allocation only happens once, which will also prevent the need to move existing elements when inserting new elements (due to the fact that you won't be exceeding your capacity multiple times).

在性能方面,您不会为小型重新分配而受到重创。使用了一些不同的常见内存模型(Visual Studio每次都需要以指数方式增加容量,以便在将来减少对它的需求)。如果你知道你将从100个元素开始,你应该在创建向量时保留空间,这样分配只发生一次,这也可以防止在插入新元素时移动现有元素的需要(因为你不会超过你的容量多次)。