C++中std::vector的push_back和emplace_back

82 阅读4分钟

首先直接上代码。

#include <cstdio>
#include <vector>

struct Widget {
  int i = 0;

  Widget() { std::printf("%p: Widget()\n", (void*)this); }
  ~Widget() { std::printf("%p: ~Widget()\n", (void*)this); }

  Widget(int i) : i(i) {
    std::printf("%p: Widget(%d)\n", (void*)this, this->i);
  }

  Widget(const Widget& w) : i(w.i) {
    std::printf("%p: Widget(const Widget&), i=%d\n", (void*)this, this->i);
  }

  Widget& operator=(const Widget& w) {
    this->i = std::move(w.i);  // NOLINT
    std::printf("%p: Widget& =operator(const Widget&), %d\n", (void*)this,
                this->i);
    return *this;
  }

  Widget(Widget&& w) noexcept : i(w.i) {
    std::printf("%p: Widget(Widget&&), i=%d\n", (void*)this, this->i);
  }

  Widget& operator=(Widget&& w) noexcept {
    if (this == &w) {
      return *this;
    }
    this->i = std::move(w.i);  // NOLINT
    std::printf("%p: Widget& =operator(Widget&&), i=%d\n", (void*)this,
                this->i);
    return *this;
  }
};

int main() {
  std::vector<Widget> V{};
  V.reserve(16);

  {
    std::printf("\npush\n");
    Widget W(Widget(3));
    V.push_back(W);
  }
  {
    std::printf("emplace\n");
    Widget W(Widget(3));
    V.emplace_back(W);
  }
  {
    std::printf("\npush\n");
    Widget W(Widget(3));
    Widget &&WR = std::move(W);
    V.push_back(WR);
  }
  {
    std::printf("emplace\n");
    Widget W(Widget(3));
    Widget &&WR = std::move(W);
    V.emplace_back(WR);
  }

  {
    std::printf("\npush\n");
    Widget W(Widget(4));
    V.push_back(std::move(W));
  }
  {
    std::printf("emplace\n");
    Widget W(Widget(4));
    V.emplace_back(std::move(W));
  }

  {
    std::printf("\npush\n");
    V.push_back(Widget(5));  // NOLINT
  }
  {
    std::printf("emplace\n");
    V.emplace_back(Widget(5));  // NOLINT
  }

  {
    std::printf("\npush\n");
    V.push_back({6});
  }
  {
    std::printf("emplace\n");
    V.emplace_back(6);
  }

  std::printf("\n---------\n");

输出结果如下:


push
0x7fff38706a40: Widget(3)
0x55be449b3820: Widget(const Widget&), i=3
0x7fff38706a40: ~Widget()
emplace
0x7fff38706a3c: Widget(3)
0x55be449b3824: Widget(const Widget&), i=3
0x7fff38706a3c: ~Widget()

push
0x7fff38706a38: Widget(3)
0x55be449b3828: Widget(const Widget&), i=3
0x7fff38706a38: ~Widget()
emplace
0x7fff38706a2c: Widget(3)
0x55be449b382c: Widget(const Widget&), i=3
0x7fff38706a2c: ~Widget()

push
0x7fff38706a1c: Widget(4)
0x55be449b3830: Widget(Widget&&), i=4
0x7fff38706a1c: ~Widget()
emplace
0x7fff38706a18: Widget(4)
0x55be449b3834: Widget(Widget&&), i=4
0x7fff38706a18: ~Widget()

push
0x7fff38706a14: Widget(5)
0x55be449b3838: Widget(Widget&&), i=5
0x7fff38706a14: ~Widget()
emplace
0x7fff38706a10: Widget(5)
0x55be449b383c: Widget(Widget&&), i=5
0x7fff38706a10: ~Widget()

push
0x7fff38706a0c: Widget(6)
0x55be449b3840: Widget(Widget&&), i=6
0x7fff38706a0c: ~Widget()
emplace
0x55be449b3844: Widget(6)

---------
0x55be449b3820: ~Widget()
0x55be449b3824: ~Widget()
0x55be449b3828: ~Widget()
0x55be449b382c: ~Widget()
0x55be449b3830: ~Widget()
0x55be449b3834: ~Widget()
0x55be449b3838: ~Widget()
0x55be449b383c: ~Widget()
0x55be449b3840: ~Widget()
0x55be449b3844: ~Widget()

从结果可以看出:

  • 对于已有的Widget,只要传入的是lvalue(包括上面的W或WR),push_back和emplace_back都调用的是copy构造函数
  • 对于可以move的表达式(即rvalue),push_back和emplace_back都调用的是move构造函数
  • emplace_back比push_back多了一个in-place的用法,这是emplace_back体现优越性的地方。这对于代码中最后的情形(传入6)。这里push_back使用了initializer_list来构造,这个用法和传入Widget(6)是一样的(即和代码中传入5使用Widget(6)的方式一样);但是对于emplace_back则可以避开push_back中调用move构造函数并且析构之前生成的对象的过程。

因此,如果只考虑性能,用emplace_back来平替push_back就可以了。

不过使用emplace_back可能会有一些bug不能够被编译器所识别。如下面的使用:

template <typename T>
using Vec = std::vector<T>;

int main() {
  {
    Vec<Vec<int>> vec(1);
    vec[0].push_back(1 << 20);     // vec[0] == {2^20}
    vec[0].emplace_back(1 << 20);  // vec[0] == {2^20, 2^20}
  }

  {
    Vec<Vec<int>> vec(1);
    // vec.push_back(1 << 20);     // 无法编译
    vec.emplace_back(1 << 20);         //  创建了大小为2^20的vector
    vec.push_back(Vec<int>(1 << 20));  // 同上
  }
}

由于emplace_back既可以初始化已有对象,也可以传入对象的构造函数来in-place构造对象。而vec.emplace_back(1 << 20)是in-place构造std::vector;由于std::vector<T>包含传入std::size_t的构造函数,因此出现了创建了大小为2^20的vector的错误。