考虑置入而非插入
如果你有个容器,持有一些,比如说,std::string
类型的对象,那么似乎合乎逻辑的做法是经由某个插入函数来向其添加新元素,而你传递给函数的元素类型将是std::string
。
说合乎逻辑倒也确实合乎逻辑,但却不一定合乎事实。
std::vector<std::string> vs;
vs.push_back("xyzzy"); //添加字符串字面量
这里,容器持有的是std::string
类型对象,但是你手头上有的是个字符串字面量。字符串字面量不是std::string
,也就是说,传递给push_back
的实参并非容器持有之物的类型。
std::vector
的push_back
针对左值和右值给出了不同的重载版本。
template<class T,
class Allocator = allocator<T>>
class vector{
public:
...
void push_back(const T& x); //插入左值
void push_back(T&& x); //插入右值
};
在下面的调用语句中:
vs.push_back("xyzzy");
编译器会看到实参类型与push_back
(std::string
的引用类型)接受的形参类型之间的不匹配。而解决之道就是通过生成代码以从字符串字面量出发创建std::string
类型的临时对象,并将该零食对象传递给push_back
。换言之,它们把这句调用看作下面这样:
vs.push_back(std::string("xyzzy")); //创建std::string类型的临时对象,并将其传递给push_back
这段代码并不高效
因为
- 从字符串字面量
xyzzy
出发,创建std::string
类型的临时对象。该对象没有名字,我们称其为temp
。针对temp
实施的构造,就是第一次的std::string
构造函数的调用,因为是个临时对象,所以temp
是个右值。 temp
被传递给push_back
的右值重载版本,在那里它被绑定到右值引用形参x
。然后,会在内存中为std::vector
构造一个x
的副本。这一次的构造(已经是第二次了)结果就是在std::vector
内创建一个新的对象(用于将x
拷贝到std::vector
中的构造函数,是移动构造函数,因为作为右值引用的x
,在复制之前被转换成了右值)。- 紧接着
push_back
返回的那一刻,temp
就被析构,所以,这就需要调用一次std::string
的析构函数。
使用emplace_back
:可以使传入的任何实参在std::vector
内构造一个std::string
。不会涉及任何临时对象。
vs.emplace_back("xyzzy"); //直接从"xyzzy"出发在vs内构造std::string类型对象
emplace_back
使用了完美转发,所以只要你没有遭遇完美转发的限制,就可以通过emplace_back
传递任意类型的任意数量和任意组合的实参。例如,如果你想通过std::string
的那个接受一个字符及重复计数的构造函数虫子啊版本来创建一个std::string
类型对象,下面的代码就可以做到。
vs.emplace_back(50, 'x'); //插入一个由50个'x'字符组成的string类型对象
emplace_back
可用于任何支持push_back
的标准容器。相似的,所有支持push_front
的标准容器也支持emplace_front
。还有,任何支持插入操作(亦即,除了std::forward_list
和std::array
以外的所有标准容器)都支持置入操作。
使得置入函数超越插入函数成为可能的,是置入函数更加灵活的接口。插入函数接受的是待插入对象,而置入函数接受的则是待插入对象的构造函数实参。这一区别就让置入函数得以避免临时对象的创建和析构,但插入函数就无法避免。
因为具备容器所持有物的实参可以被传递给一个置入函数(该实参数会引发函数执行拷贝或者移动构造),所以即使在插入函数并不要求创建临时对象的情况下,也可以使用置入函数。在那种情况下,插入函数和置入函数本质上做的是同一件事。
例如,给出下面的std::string
类型对象
std::string queenOfDisco("Donna Summer");
下面两个调用语句都成立,并且对于容器来说净效果相同。
vs.push_back(queenOfDisco);
vs.emplace_back(queenOfDisco);
这样一来,置入函数就能做到插入函数所能做到的一切。有时,他们可以比后者做的更高效一些。而且,至少在理论上,它们不会比后者效率更低。
从理论上来说,理论和实践没有区别:但从实践上来说,理论和实践是有区别的。从标准库的当前实现上来看,在有些情况下,正如预期的那样,置入的性能优于插入,但是,不幸的是,还是存在插入函数运行得更快的情况。后面一种情况不太容易表现,因为具体来说,取决于传递的实参类型,使用的容器种类,请求插入或置入的容器位置,所持有类型构造函数的异常安全性等等。这里适用一般的性能调优建议:欲确定置入或插入哪个运行得更快,需要对两者实施基准测试。
如果下列情况都成立,那么置入将几乎比插入更高效。
- 欲添加的值是以构造而非赋值方式加入容器。
要点速记
- 从原理上说,置入函数应该有时比对应的插入函数高效,而且不应该有更低效的可能。
- 从实践上说,置入函数在以下几个前提成立时,极有可能会运行得更快:(1)待添加的值是以构造而非赋值方式加入容器;(2)传递的实参类型与容器持有物的类型不同;(3)容器不会由于存在重复值而拒绝待添加的值。
- 置入函数可能会执行在插入函数中被拒绝的类型转换。