c++ push_back与emplace_back
push_back与emplace_back
本节直接讨论在向容器添加数据时,插入(push_back、push_front、insert等)和置入(emplace_back)的内存性能情况,深入了解C++的内部机制。考虑下面代码:
vector<string> vs;
vs.push_back("abcd");
push_back的两个版本:
void push_back(const T& x);//左值
void push_back(T&& x);//右值
vs是一个容器,内部持有的是string类型对象,但此时“abcd”不是string类型,而是const char[5]。编译器此时看到实参(“abcd”)和push_back接受的形参(string)类型不匹配。此时,编译器会创建string临时变量(调用string的构造函数),并将该临时变量传给push_back。可看作如下方式:
vs.push_back(string("abcd"));
这段代码的详细执行流程如下:
1、从"abcd"出发创建string类型的临时变量。该对象没有名字(为方便描述,我们取名temp),是一个右值,此过程第一次调用string的构造函数,"abcd"作为参数。
2、temp被传递给push_back的右值重载版本,此时它被绑定到右值引用形参x,然后会在内存为vector构造一个x的副本(第二次构造),结果在vector内创建了一个新的对象,该对象用于将x复制到vector中的构造函数,是移动构造函数,因为作为右值引用的x在复制之前被转换成了右值。
3、push_back返回的那一刻,temp被析构,需要调用string的析构函数。
为了达到效率的最大化,避免先构造再析构temp,可以调用置入函数emplace_back:它使用传入的任何实参在vector内构造string,不涉及任何临时变量。
vs.emplace_back("abcd");
插入函数接受待插入对象,置入函数接受的是待插入对象的构造函数实参,使得临时对象的创建和析构得以避免。同时,置入函数也能做到插入函数能做到的一切。
当以下情况都成立,置入函数肯定是优于插入函数:
1、欲添加的值是以构造而非赋值的方式加入容器。
vector<string> vs;
... //向vs中添加元素
vs.emplace(vs.begin(),"abcd");//vs开头添加值
欲添加的值添加到vs结尾很显然是需要构造的,因为该位置无对象。若添加的不是尾部,而是其他位置,如上面的代码。此时会采用移动赋值的方式让"abcd"添加到开头,移动源是新创建的一个临时对象,此时置入操作的优势就没有了,依然构造了临时对象。
基于节点的容器几乎总是采用构造来添加新值,除了vector、deque和string。
2、传递的实参类型和容器持有的类型不同。很容易理解,如果实参类型完全匹配,就不需要额外的操作,直接添加即可,此时置入操作的优势也不存在。
3、容器不会由于存在重复值而拒绝待添加的值。
要检测某值是否在容器中,置入的实现是会使用该值创建一个节点,用该节点与容器中的现有节点比较,若不在容器中,则直接添加到容器,若容器中已有该节点,则删除该节点。这里就有一个构造和析构的成本,此时置入操作的优势也不存在了。
**然而,凡事无绝对。并不是在任何时候,push_back性能都是优于emplace_back的。**以下两种情况证明上述观点:
1、处理带有资源管理器的容器。
list<shared_ptr<Widget>> ptrs;
假设想向ptrs中添加一个自定义删除器释放内存的shared_ptr,此时就必须使用new而非make_shared。删除函数定义如下:
void killWidget(Widget* pWidget);
此时,插入函数:
ptrs.push_back(shared_ptr<Widget>(new Widget,killWidget));
或
ptrs.push_back({new Widget,killWidget});
假设发生这种情况:
1、上述的插入操作都会构shared_ptr类型的临时对象(temp),用来持有从“new Widget”返回的裸指针。
2、push_back会按照引用的方式接受temp。在为链表节点分配内存以持有temp副本的过程中,抛出了内存不足的异常。
3、该异常传播到push_back之外,temp被析构,killWidget会自动析构Widget。
上述过程即使发生异常,但是资源没有泄露。若使用emplace_back,则:
1、从"new Widget"返回的裸指针被完美转发,运行到emplace_back内为链表节点分配内容的执行点。然后内存分配失败,抛出内存不足的异常。
2、该异常传播到push_back之外,此时唯一能获取堆上Widget的裸指针丢失了,Widget发生资源泄露。
在调用持有资源管理对象的容器的插入函数时,函数的形参类型通常能确保在资源的获取和对资源管理的对象实施构造之间不会有任何动作。在置入函数中,完美转发会推迟资源管理对象的创建,直到他们能够在容器的内存中构造为止,此时就会出现因发生异常而导致资源泄露的情况。
2、处理带有explicit声明的构造函数。
vector<regex> regexs;//存储正则表达式的容器
regexs.push_back(nullptr);//编译失败
regexs.emplace_back(nullptr);//编译成功
由于nullptr并不是正则表达式,在第一种情况下就要求从指针到regex的隐式类型转换,由于构造函数定义为explicit,因此无法转换,从而编译失败。
而在emplace_back的调用过程中,向regex对象传入的是构造函数实参,因此会被编译期看作:regex r(nullptr);从而通过编译。当然,本身nullptr就不是正则表达式,编译虽然通过,但会造成运行的崩溃。