关于emplace_back和push_back网上有种说法,“emplace_back比push_back效率更高”,具体原因是emplace_back相比push_back减少了一次临时变量的拷贝构造。事实上大多数情况下,emplace_back和push_back表现是一致的。
此外本文还验证了《Morden Effective C++》一书中条款14,“只要函数不会发射异常,就为其加上noexcept声明”。
实验准备
首先定义一个Element
类型
class Element{
public:
Element(){
id_ = ++cnt_;
std::cout << "default ctor " << id_ << std::endl;
}
~Element(){
std::cout << "dtor " << id_ << std::endl;
}
Element(int i){
id_ = ++cnt_;
std::cout << "ctor(int) " << id_ << std::endl;
}
Element(int i, char c){
id_ = ++cnt_;
std::cout << "ctor(int, char)" << id_ << std::endl;
}
Element(const Element& rhs){
id_ = ++cnt_;
std::cout << "copy ctor " << id_ << " from " << rhs.id_ << std::endl;
}
Element(Element&& rhs){
id_ = ++cnt_;
std::cout << "move ctor " << id_ << " from " << rhs.id_ << std::endl;
}
static int cnt_;
int id_;
};
int Element::cnt_ = 0;
Element
类定义了不同的构造函数,静态变量cnt_
用来为不同的实例的id_
赋值。
emplace_back和push_back
插入左值
首先测试插入左值的情况,下面是push_back
的测试代码。
std::vector<Element> vec;
Element e;
vec.push_back(e);
下面是emplace_back
的测试代码
std::vector<Element> vec;
Element e;
vec.emplace_back(e);
二者输出完全一致,可以看到当插入左值时,表现完全一致,都是调用拷贝构造。
default ctor 1
copy ctor 2 from 1
dtor 1
dtor 2
插入右值
首先测试插入默认构造函数返回的右值,首先是push_back
版本
std::vector<Element> vec;
Element e;
vec.push_back(e);
下面是emplace_back
版本
std::vector<Element> vec;
Element e;
vec.emplace_back(e);
两者输出完全一致,都是先调用构造函数,再调用移动构造
default ctor 1
move ctor 2 from 1
dtor 1
dtor 2
换一个构造函数试试
std::vector<Element> vec;
vec.push_back(Element(1, 'a'));
// vec.emplace_back(Element(1, 'a'));
两者的输出同样完全一致,都是先调用构造函数,再调用移动构造
ctor(int, char)1
move ctor 2 from 1
dtor 1
dtor 2
插入时构造
区别在于emplace_back
是使用可变参数模板定义的
template<class... _Valty>
void emplace_back(_Valty&&... _Val)
当使用下面这样的插入形式时
vec.emplace_back(1, 'a');
// vec.push_back(1, 'a');
push_back
版本会报错,因为push_back
没有实现可变参数。emplace_back
的输出为
ctor(int, char)1
dtor 1
可以看到,新的实例直接在vector内部调用了构造函数,省去了移动构造的步骤。
要想调用push_back
函数,则需要通过下面的代码。
std::vector<Element> vec;
vec.emplace_back(Element(1, 'a'));
输出如下
ctor(int, char)1
move ctor 2 from 1
dtor 1
dtor 2
如你所料,先后调用了构造函数和移动构造函数,这也是push_back
和emplace_back
在插入时表现的唯一区别,而这个区别是由程序员编码方式产生的。
验证noexcept
先说结论,如果实现了不抛异常的移动构造函数,vector
扩容时会调用移动构造函数,否则调用拷贝构造函数
参照Element
类实现一个具有不抛异常的移动构造函数的ElementNoexp
class ElementNoexp{
public:
ElementNoexp(){
id_ = ++cnt_;
std::cout << "default ctor " << id_ << std::endl;
}
~ElementNoexp(){
std::cout << "dtor " << id_ << std::endl;
}
ElementNoexp(int i){
id_ = ++cnt_;
std::cout << "ctor(int) " << id_ << std::endl;
}
ElementNoexp(int i, char c){
id_ = ++cnt_;
std::cout << "ctor(int, char)" << id_ << std::endl;
}
ElementNoexp(const ElementNoexp& rhs){
id_ = ++cnt_;
std::cout << "copy ctor " << id_ << " from " << rhs.id_ << std::endl;
}
ElementNoexp(ElementNoexp&& rhs) noexcept{
id_ = ++cnt_;
std::cout << "move ctor " << id_ << " from " << rhs.id_ << std::endl;
}
static int cnt_;
int id_;
};
测试代码
std::vector<ElementNoexp> vec;
vec.resize(3);
std::cout << "----------------------------------" << std::endl;
vec.emplace_back(ElementNoexp(1, 'a'));
没有noexcept
声明版本的输出
default ctor 1
default ctor 2
default ctor 3
----------------------------------
ctor(int, char)4
move ctor 5 from 4
copy ctor 6 from 1
copy ctor 7 from 2
copy ctor 8 from 3
dtor 1
dtor 2
dtor 3
dtor 4
----------------------------------
dtor 6
dtor 7
dtor 8
dtor 5
noexcept
版本输出
default ctor 1
default ctor 2
default ctor 3
----------------------------------
ctor(int, char)4
move ctor 5 from 4
move ctor 6 from 1
dtor 1
move ctor 7 from 2
dtor 2
move ctor 8 from 3
dtor 3
dtor 4
----------------------------------
dtor 6
dtor 7
dtor 8
dtor 5
可以看到声明了noexcept
版本移动构造函数的ElementNoexp
在扩容过程中是边移动边析构,而没有声明noexcept
的函数则是先确保拷贝构造完成,再释放原成员。
因此,当确保移动构造不会抛出异常时,将移动构造声明为noexcept
可以提高程序的执行效率。
网友评论