push_back 和 emplace_back 剖析

文章目录

  • 前言
  • 一、源码
  • 二、共同点
  • 三、优化点
  • 总结


前言

仓库推荐

C/C++ 每日一练小仓库,慢慢学习C++ 知识必备仓库 https://github.com/yeshenyong/practice_cpp

C++ wiki_wiki 万能仓库,正在持续更新,由0 - 0.x 持续学习C++ 接口与对应知识仓库 https://github.com/yeshenyong/Wiki_Wiki

正文:

摘自网上:emplace_back() 和 push_back() 的区别,就在于底层实现的机制不同。push_back() 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);而 emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程

说的对,又有点不对(真的都省了吗?什么情况下才省)

网上一直把 push_back说的贼菜贼菜,emplace_back 性能贼好贼好。本着:“难道之前的人都是傻子,不直接把push_back 实现成 emplace_back 的样子” 的观点,往下看。

ps:

  1. 下面说的一样适用于其它 STL 容器,举一反三
  2. vector 是深拷贝!!!

一、源码

想要研究一下,那就不能被网上的文章迁入太深的误区… (百度必应都会搜出一堆乱写的答案,那我为啥不可以用chatGPT 呢?)
那就直接上源码咯

vector<_Tp, _Allocator>::emplace_back(_Args&&... __args)
{
    if (this->__end_ < this->__end_cap())
    {
        __construct_one_at_end(_VSTD::forward<_Args>(__args)...);
    }
    else
        __emplace_back_slow_path(_VSTD::forward<_Args>(__args)...);		// 扩容触发
#if _LIBCPP_STD_VER > 14
    return this->back();
#endif
}

void
vector<_Tp, _Allocator>::push_back(value_type&& __x)
{
    if (this->__end_ < this->__end_cap())
    {
        __construct_one_at_end(_VSTD::move(__x));
    }
    else
        __push_back_slow_path(_VSTD::move(__x));		// 扩容触发
}

void
vector<_Tp, _Allocator>::push_back(const_reference __x)
{
    if (this->__end_ != this->__end_cap())
    {
        __construct_one_at_end(__x);
    }
    else
        __push_back_slow_path(__x);		// 扩容触发
}

粗略看完了源码
push_back 的实现有两种函数

push_back(value_type&& __x);
push_back(const_reference __x); // const value_type&

emplace_back 的实现只有一种函数

emplace_back(_Args&&… __args);

发现,咦!怎么emplace_back push_back 只有函数参数类型不一致,函数内部触发机制基本雷同(不考虑扩容逻辑)

  1. emplace_back它怎么实现的跟网上说那种,在vector的末尾直接创建这个元素就可以嘞

  2. push_back为啥就要容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中

带着两个疑问,逐层剖析 push_back

来个例子呗:(注意:这个例子是基于push_back缺点描述的)

#include 
#include 

using namespace std;

class Base {
    public:
        Base(int i) : i(i) {
            cout << "Base()" << endl;
        }
        ~Base() {
            cout << "~Base()" << endl;
        }
        Base(const Base& other) noexcept {
            this->i = other.i;
            cout << "Base(const Base& other)" << endl;
        }
        Base(Base&& other) noexcept {
            this->i = other.i;
            cout << "Base(Base&& other)" << endl;
        }
    public:
        int i;
};


int main() {
    vector<Base> v;
    v.reserve(20);
    cout << "push_back\n";
    for (int i = 0; i < 1; i++) {
        cout << "i = " << i << endl;
        v.push_back(Base(i));
    }
}

输出:

push_back
i = 0
Base()								// Base(i) 是右值,构造临时对象,承接
Base(Base&& other)
~Base()
~Base()

第一个问题:创建元素

Base(i) 是右值,构造临时对象,承接下面这个push_back
push_back(value_type&& __x);
所以必须提前构造出来,进行传参(关键点:传参中临时对象会被const value_type& 承接对象)

第二个问题:移动构造/元素拷贝

前提:现在已经构造好了要插入的元素咯

那接下来要干嘛,直接复制构造函数/移动构造函数 不就OK了吗

走移动构造逻辑(vector 是深拷贝)构造在vector 尾部即可

现在触发的情况是

  1. 类隐式构造(Base(int i)
  2. push_back

这个前提下,会触发网上部分观点所说的

push_back() 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);而 emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程

那又有问题了,为什么emplace_back就可以规避掉第一个创建元素的临时对象,直接做第二步构造函数呢

刚刚提到它们两个的区别在于传参

emplace_back(_Args&&… __args);
push_back(value_type&& __x);
push_back(const_reference __x); // const value_type&

emplace_back 取而代之是一个可变的参数列表,所以这意味着可以完美地转发参数并直接将一个对象构造到一个容器当中

  1. 可变参数列表
  2. 完美转发(保持右值变量)
    这非常有用,因为无用 RVO(return value optimized)和移动语义多么美妙,仍然存在 push_back 可能进行不必要的复制(或移动)的复杂情况
    (具体原理,此处不加以叙说,若有多人提问,再补写吧…)

二、共同点

以下情况下,用push_backemplace_back 是一样的,没有任何区别(走的函数有区别。。。)

  1. 容器类型为基础类型
  2. 容器类型中的类不存在隐式构造函数(explict 声明)或者为无参构造函数(意味着传入为右值的容器值)
  3. 插入元素不为右值(无需构造)

情况2:实验代码

#include 
#include 

using namespace std;

class Child {
    public:
        Child() {
            cout << "Child()" << endl;
        }
        ~Child() {
            cout << "~Child()" << endl;
        }
        Child(const Child& other) noexcept {
            cout << "Child(const Child& other)" << endl;
        }
        Child(Child&& other) noexcept {
            cout << "Child(Child&& other)" << endl;
        }
};

class Base {
    public:
        explicit Base(int i) : i(i) {
            cout << "Base()" << endl;
        }
        ~Base() {
            cout << "~Base()" << endl;
        }
        Base(const Base& other) noexcept {
            this->i = other.i;
            cout << "Base(const Base& other)" << endl;
        }
        Base(Base&& other) noexcept {
            this->i = other.i;
            cout << "Base(Base&& other)" << endl;
        }
    public:
        int i;
};


int main() {

    vector<Base> v;
    vector<Child> x;
    x.reserve(20);
    v.reserve(20);
    cout << "base push_back\n";
    v.push_back(Base(1));

    cout << "base emplace_back\n";
    v.emplace_back(Base(1));

    cout << endl;

    cout << "child push_back\n";
    x.push_back(Child());

    cout << "child emplace_back\n";
    x.emplace_back(Child());


    cout << endl;
    return 0;
}

输出

base push_back
Base()
Base(Base&& other)
~Base()
base emplace_back
Base()
Base(Base&& other)
~Base()

child push_back
Child()
Child(Child&& other)
~Child()
child emplace_back
Child()
Child(Child&& other)
~Child()

~Child()
~Child()
~Base()
~Base()

情况3:实验代码

#include 
#include 

using namespace std;

class Child {
    public:
        Child() {
            cout << "Child()" << endl;
        }
        ~Child() {
            cout << "~Child()" << endl;
        }
        Child(const Child& other) noexcept {
            cout << "Child(const Child& other)" << endl;
        }
        Child(Child&& other) noexcept {
            cout << "Child(Child&& other)" << endl;
        }
};

int main() {
    vector<Child> x;
    x.reserve(20);
    Child a;
    Child b;
    cout << endl;
    cout << "child push_back\n";
    x.push_back(a);
    cout << "child emplace_back\n";
    x.emplace_back(b);

    cout << endl;
    return 0;
}

输出

Child()
Child()

child push_back
Child(const Child& other)
child emplace_back
Child(const Child& other)

~Child()
~Child()
~Child()
~Child()

三、优化点

触发的情况是

  1. 类隐式构造(Base(int i)
  2. push_back

emplace_back 就是通过可变参数列表进行完美转发,来减少不必要构造的问题

#include 
#include 

using namespace std;

class Base {
    public:
        Base(int i) : i(i) {
            cout << "Base()" << endl;
        }
        ~Base() {
            cout << "~Base()" << endl;
        }
        Base(const Base& other) noexcept {
            this->i = other.i;
            cout << "Base(const Base& other)" << endl;
        }
        Base(Base&& other) noexcept {
            this->i = other.i;
            cout << "Base(Base&& other)" << endl;
        }
    public:
        int i;
};


int main() {

    vector<Base> v;
    v.reserve(20);
    cout << "push_back\n";
    v.push_back(1);

    cout << "emplace_back\n";
    v.emplace_back(1);
    return 0;
}

输出

push_back
Base()
Base(Base&& other)
~Base()
emplace_back
Base()
~Base()
~Base()

总结

回到初衷

本着:“难道之前的人都是傻子,不直接把push_back 实现成 emplace_back 的样子” 的观点

为什么之前不直接设计为emplace_back这种模式?还那么烦搞来搞去

原因:可变参数模板之前没有友好的实现(不是我说的,别人说的,所以这句话很对 https://stackoverflow.com/questions/4303513/push-back-vs-emplace-back)

浅析一下两者而已,编程还是要从实用性出发

最后:能用emplace_back 直接用emplace_back(无脑)

你可能感兴趣的:(C++,c++,开发语言,后端)