push_back 和 emplace_back

网络上讲这两个操作差异的文章很多,这里仅从使用差异分析。

定义

cpp中vector的push_back和emplace_back精简小结_构造函数


cpp中vector的push_back和emplace_back精简小结_c++_02

假设:

  1. 控制变量:当前vector能够容下push_back和emplace_back的所有元素,没有触发扩容操作。 使用vector.reserve();
  2. push_back和emplace_back操作的对象类型:
  1. 普通变量、普通变量
  2. 普通变量、临时变量
  3. 临时变量、普通变量
  4. 临时变量、临时变量

实验的类Foo

#include <iostream>
#include <vector>

class Foo {
public:
    // default ctor
    Foo(int value = 0) : value_(value) {
        std::cout << "Foo(int value = 0)" << std::endl;
    }

    // copy ctor
    Foo(const Foo& foo) : value_(foo.value_) {
        std::cout << "Foo(const Foo& foo)" << std::endl;
    }

    // move ctor
    Foo(Foo&& foo) : value_(foo.value_) {
        foo.value_ = 0;
        std::cout << "Foo(Foo&& foo)" << std::endl;
    }

    // copy assignment 
    Foo& operator=(const Foo& foo) {
        value_ = foo.value_;
        std::cout << "Foo& operator=(const Foo& foo)" << std::endl;
        return *this;
    }

    // move assignment
    Foo& operator=(Foo&& foo) {
        value_ = foo.value_;
        foo.value_ = 0;
        std::cout << "Foo& operator=(Foo&& foo)" << std::endl;
        return *this;
    }
	// dtor
    ~Foo() {
        std::cout << "~Foo()" << std::endl;
    }

private:
    int value_;
};

1. 普通变量、普通变量

int main() {
    std::vector<Foo> vFoos;
    vFoos.reserve(2);       // 防止vector扩容
    std::cout << vFoos.capacity() << std::endl;
    Foo foo(20);            // 普通变量
    std::cout << "----------------------------------" << std::endl;
    vFoos.push_back(foo);    
    std::cout << "----------------------------------" << std::endl;
    vFoos.emplace_back(foo);
    std::cout << "----------------------------------" << std::endl;
}

输出:

2
Foo(int value = 0)
----------------------------------
Foo(const Foo& foo)
----------------------------------
Foo(const Foo& foo)
----------------------------------
~Foo()
~Foo()
~Foo()

分析:
两者目标就是把普通变量foo的信息放到vector上,但是foo可能后续还得用,所以不能强行移动,只能使用拷贝构造函数。


2. 普通变量、临时变量

int main() {
    std::vector<Foo> vFoos;
    vFoos.reserve(2);       // 防止vector扩容
    std::cout << vFoos.capacity() << std::endl;
    Foo foo(20);            // 普通变量
    std::cout << "----------------------------------" << std::endl;
    vFoos.push_back(foo);    
    std::cout << "----------------------------------" << std::endl;
    vFoos.emplace_back(1);
    std::cout << "----------------------------------" << std::endl;
}

输出:

2
Foo(int value = 0)
----------------------------------
Foo(const Foo& foo)
----------------------------------
Foo(int value = 0)
----------------------------------
~Foo()
~Foo()
~Foo()

分析:
可以看见emplace_back是直接在vector管理的堆上内存原地调用构造函数。

3. 临时变量、普通变量

int main() {
    std::vector<Foo> vFoos;
    vFoos.reserve(2);       // 防止vector扩容
    std::cout << vFoos.capacity() << std::endl;
    Foo foo(20);            // 普通变量
    std::cout << "----------------------------------" << std::endl;
    vFoos.push_back({1});    
    std::cout << "----------------------------------" << std::endl;
    vFoos.emplace_back(foo);
    std::cout << "----------------------------------" << std::endl;
}

输出

2
Foo(int value = 0)
----------------------------------
Foo(int value = 0)
Foo(Foo&& foo)
~Foo()
----------------------------------
Foo(const Foo& foo)
----------------------------------
~Foo()
~Foo()
~Foo()

分析:
push_back先根据传入的实参{1}调用构造函数以创建一个栈上的临时对象,然后使用移动/拷贝构造函数将其信息放到vector管理的堆上。这里是使用移动还是拷贝构造比较讲究?换位思考,编译器是能用移动就绝不用拷贝。可能在这个实验的类里两个构造函数的工作类似,但是移动构造对于管理堆上内存的类而言是远比拷贝构造轻量的。深拷贝?

4. 临时变量、临时变量

int main() {
    std::vector<Foo> vFoos;
    vFoos.reserve(2);       // 防止vector扩容
    std::cout << vFoos.capacity() << std::endl;
    std::cout << "----------------------------------" << std::endl;
    vFoos.push_back({1});    
    std::cout << "----------------------------------" << std::endl;
    vFoos.emplace_back(1);
    std::cout << "----------------------------------" << std::endl;
}

输出:

2
----------------------------------
Foo(int value = 0)
Foo(Foo&& foo)
~Foo()
----------------------------------
Foo(int value = 0)
----------------------------------
~Foo()
~Foo()

分析:
个人认为是emplace_back真正彰显性能优势的场景。它只需要做一件事,通过用户提供的实参1在vector管理的堆上调用类的构造函数即可。而push_back还是避免不了地要构造临时对象,不过它也在尽力优化地调用移动构造而非拷贝构造(如果可以使用移动构造的话)。

总结

  1. 如果操作的是临时对象,那么这是emplace_back的用武之地(假设不会触发动态扩容)。
  2. push_back总是会构造临时对象,然后析构它。不过它也在尽力的优化:去使用移动构造函数而非拷贝构造函数。

待优化的地方

  1. 构造一个较重的类以量化两个操作的优势?