0%

push_back和emplace_back

modern C++中提到使用考虑使用置入代替插入,个人理解就是使用考虑使用emplace_back代替push_back操作,那么这两者之间究竟有什么区别,想探究一下

首先这两个函数的定义是不一样的

函数定义

push_back是有两个函数的(重载),一个接受左值一个接受右值,并且接受右值后进行了move

1
2
3
4
5
6
7
8
_CONSTEXPR20 void push_back(const _Ty& _Val) { // insert element at end, provide strong guarantee
_Emplace_one_at_back(_Val);
}

_CONSTEXPR20 void push_back(_Ty&& _Val) {
// insert by moving into element at end, provide strong guarantee
_Emplace_one_at_back(_STD move(_Val));
}

emplace_back是只有一个函数,是一个模板函数,参数是一个通用引用并且是变长参数,然后进行了完美转发forward

1
2
3
4
5
6
7
8
9
10
11
    template <class... _Valty>
_CONSTEXPR20 decltype(auto) emplace_back(_Valty&&... _Val) {
// insert by perfectly forwarding into element at end, provide strong guarantee
// 完美转发直接将参数传入内部
_Ty& _Result = _Emplace_one_at_back(_STD forward<_Valty>(_Val)...);
#if _HAS_CXX17
return _Result;
#else // ^^^ _HAS_CXX17 / !_HAS_CXX17 vvv
(void) _Result;
#endif // _HAS_CXX17
}

此外,push_backemplace_back都使用了_Emplace_one_at_back进行插入

差异

性能上

先说结论:

理论上来说,emplace_backpush_back效率更高。emplace_back能够在vector内部构建元素,从而减少拷贝或者移动操作

这句话怎么理解呢,举个例子

1
2
std::vector<std::string> vs;        //std::string的容器
vs.push_back("xyzzy"); //添加字符串字面量

通过上面的源码我们可以看到push_back接受的参数是一个T的元素,但是这里传入的是字面量,所以在这里会通过字面量创建出一个临时变量(隐式转换),等价于下面的代码

1
vs.push_back(std::string("xyzzy")); //创建临时std::string,把它传给push_back

综上vs的push_back总共有三个操作

  1. 一个std::string的临时对象从字面量“xyzzy”被创建。这个对象没有名字,我们可以称为temptemp的构造是第一次std::string构造。因为是临时变量,所以temp是右值。
  2. temp被传递给push_back的右值重载函数,绑定到右值引用形参_Val。在std::vector的内存中一个_Val的副本被创建。这次构造——也是第二次构造——在std::vector内部真正创建一个对象。
  3. push_back返回之后,temp立刻被销毁,调用了一次std::string的析构函数。

当我们使用emplace_back时,

1
vs.emplace_back("xyzzy");           //直接用“xyzzy”在vs内构造std::string

emplace_back使用完美转发将”xyzzy”传入了vector内部(就是前面_Ty& _Result = _Emplace_one_at_back(_STD forward<_Valty>(_Val)...);),直接在内部的数组的末尾构建元素插入,减少了临时变量的产生,提高了效率。

接受参数上

emplace_back使用完美转发,因此只要你没有遇到完美转发的限制(完美转发也会失败,在这里不多讲解),就可以传递任何实参以及组合到emplace_back

比如

1
2
3
vs.emplace_back(50, 'x');           //插入由50个“x”组成的一个std::string
vs.push_back(50, 'x'); // error
vs.push_back(std::string(50, 'x')) // fine

再比如下面这种情况,临时变量都不给你转化,只能用emplace_back传入

1
2
3
4
5
6
7
8
9
10
11
12
13
class A
{
public:
explicit A(int a) :m_a(a) {

}
private:
int m_a;
};

std::vector<A> aVec;
aVec.push_back(1); // error
aVec.emplace_back(1); // fine

所以写起来emplace_back肯定是更加舒服的,少写好多字母(理论上减少出错)。

代码实验

实验一(emplace_back高效性)

使用push_back

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class BaseClass
{
public:
BaseClass(const std::string name) : name_(name)
{
std::cout << name_ << " constructor called" << std::endl;
}
BaseClass(const BaseClass& b) :name_(b.name_)
{
std::cout << name_ << " copy constructor called" << std::endl;
}

BaseClass(BaseClass&& b)
{
// 此处只是演示,并未进行真正移动
name_ = b.name_;
b.name_ = b.name_ + " have move";
std::cout << name_ << " move constructor called" << std::endl;
}
virtual ~BaseClass()
{
std::cout << name_ << " destructor called" << std::endl;
}
private:
std::string name_;
};

int main(int argc, char** argv)
{
std::vector<BaseClass> bcVec;
std::cout << "--------------------------------push_back :" << std::endl;
bcVec.push_back(BaseClass("push_back_obj"));
// push_back:
// (1) 调用 有参构造函数 BaseClass (const std::string name) 创建临时对象;
// (2)调用 移动构造函数 BaseClass(BaseClass&& b) 到vector中;
// (3) 调用 析构函数 销毁临时对象;
std::cout << "--------------------------------destruct:" << std::endl;
// (4) vector进行析构,调用析构函数
}

运行结果符合预期

使用emplace_back

1
2
3
4
5
6
7
8
9
10
int main(int argc, char** argv)
{
std::vector<BaseClass> bcVec;
std::cout << "--------------------------------emplace_back :" << std::endl;
bcVec.emplace_back("emplace_back_obj");
// (1) 在vector中直接调用构造函数创建元素
std::cout << "--------------------------------destruct:" << std::endl;
// (2) vector进行析构,调用析构函数
}

运行结果

可以看得出来emplace_back少临时变量的构造、移动、销毁操作,效率要高一些

实验二(两者都传入右值)

如果传入右值,push_back 和 emplace_back效率相同,都会有临时变量产生的构造、移动、销毁操作。

push_back传入右值:

1
2
3
4
5
6
7
8
std::cout << "--------------------------------push_back rvalue:" << std::endl;
bcVec.push_back(BaseClass("push_back_rvalue"));
// push_back:
// (1) 调用 有参构造函数 BaseClass (const std::string name) 创建临时对象;
// (2)调用 移动构造函数 BaseClass(BaseClass&& b) 到vector中;
// (3) 调用 析构函数 销毁临时对象;
std::cout << "--------------------------------destruct:" << std::endl;
// (4) vector进行析构,调用析构函数

上面已经展示过了,这里就不多解释了。

emplace_back传入右值:

1
2
3
4
5
6
7
8
9
10
11
12
13
int main(int argc, char** argv)
{
std::vector<BaseClass> bcVec;

std::cout << "--------------------------------emplace_back rvalue:" << std::endl;
bcVec.emplace_back(BaseClass("emplace_back_rvalue"));
// (1) 调用 有参构造函数 BaseClass (const std::string name) 创建临时对象;
// (2)调用 移动构造函数 BaseClass(BaseClass&& b) 到vector中;
// (3) 调用 析构函数 销毁临时对象;
std::cout << "--------------------------------destruct:" << std::endl;
// (4) vector进行析构,调用析构函数

}

运行结果,可以看出效率没有提高

实验三(两者都传入左值)

如果传入右值,push_back 和 emplace_back效率相同,两者都会调用拷贝构造函数

push_back传入左值:

1
2
3
4
5
6
7
8
9
10
11
12
13
int main(int argc, char** argv)
{
std::vector<BaseClass> bcVec;
std::cout << "--------------------------------push_back lvalue:" << std::endl;
// (1) 调用 有参构造函数 BaseClass (const std::string name) 创建obj对象;
BaseClass obj("obj");
// (2) 调用 拷贝构造函数;
bcVec.push_back(obj);
std::cout << "--------------------------------destruct:" << std::endl;
// (3) obj被析构,调用BaseClass的析构函数
// (4) vector被析构,其中的元素调用BaseClass的析构函数

}

emplace_back传入左值:

1
2
3
4
5
6
7
8
9
10
11
12
int main(int argc, char** argv)
{
std::vector<BaseClass> bcVec;
std::cout << "--------------------------------emplace_back lvalue:" << std::endl;
// (1) 调用 有参构造函数 BaseClass (const std::string name) 创建obj对象;
BaseClass obj("obj");
// (2) 调用 拷贝构造函数;
bcVec.emplace_back(obj);
std::cout << "--------------------------------destruct:" << std::endl;
// (3) obj被析构,调用BaseClass的析构函数
// (4) vector被析构,其中的元素调用BaseClass的析构函数
}

参考: