C++中push_back与emplace_back的区别
一、简介
在C++中我们常会用到STL容器,如vector、map、list、set等。其中vector容器更为常见,在使用vector容器时,我们常会遇到一个需求就是在容器尾部插入一个元素,这个时候就需要用到
vector的内部成员函数如insert()、push_back()和emplace_back()等,insert()不在本篇文章的介绍范围内,仅仅对、push_back()和emplace_back()作更详细的说明。
二、push_back()的用法
push_back()的使用比较简单,仅仅将需要插入的元素传入这个函数即可,我们通过一段示例代码作演示。
#include <vector>
#include <iostream>
int main()
{
std::vector<int> vec_1 = {1,2,3};
vec_1.push_back(4);
vec_1.push_back(5);
vec_1.push_back(6);
for(auto ele: vec_1)
{
std::cout << ele << std::endl;
}
}
把上面的代码复制到
运行结果:
1
2
3
4
5
6
可见每执行一次push_back(),便可将元素插入到容器最后一个位置。
三、emplace_back()的用法
同样emplace_back()的使用也比较简单,也是仅仅将需要插入的元素传入这个函数即可,我们通过一段示例代码作演示。
#include <vector>
#include <iostream>
int main()
{
std::vector<int> vec_1 = {1,2,3};
vec_1.emplace_back(4);
vec_1.emplace_back(5);
vec_1.emplace_back(6);
for(auto ele: vec_1)
{
std::cout << ele << std::endl;
}
}
把上面的代码复制到
运行结果:
1
2
3
4
5
6
可见每执行一次emplace_back(),便可将元素插入到容器最后一个位置。
四、push_back()和emplace_back()的差异
对比上面示例代码运行的结果,我们发现两个函数的运行结果是一样的,都是将元素插入到容器的最后。那么,既然如此是不是就意味着push_back()和emplace_back()是一样的呢?
当然不是。
先让我通过下面两段代码进行对比:
#include <vector>
#include <iostream>
class Test
{
public:
//默认构造函数
Test(int val):val(val)
{
std::cout << "===> 构造函数" << std::endl;
}
// //拷贝构造函数
Test(const Test& test):val(test.val)
{
std::cout << "=====> 拷贝构造函数" << std::endl;
}
//移动构造函数
Test(Test&& test):val(test.val)
{
std::cout << "=====> 移动构造函数" << std::endl;
}
void printVal()
{
std::cout << val << std::endl;
}
private:
int val;
};
int main()
{
std::vector<Test> testVec1;
std::cout << "test push_back: " << std::endl;
testVec1.push_back(1);
std::vector<Test> testVec2;
std::cout << "test emplace_back: " << std::endl;
testVec2.emplace_back(5);
}
运行代码后我们看到如下输出:
test push_back:
===> 构造函数
=====> 移动构造函数
test emplace_back:
===> 构造函数
从上面的结果可见,当使用push_back()向容器中插入元素时,会先调用元素类的构造函数,然后再调用移动构造函数,将元素插到容器尾部。而使用emplace_back()时,则直接调用构造构造函数,直接在容器尾部构造元素,而无需再调用移动构造函数或拷贝构造函数。这种机制对于复杂对象的操作,能更好地优化性能,降低资源的消耗。
五、内部实现
这部分我们通过对比push_back()和emplace_back()的内部实现进一步说明二者的差异。
首先看一下push_back()的内部实现,用一段简化版的push_back()模板函数做演示:
template <typename T>
class vector {
private:
T* data_; // 指向元素的指针
size_t size_; // 当前元素数量
size_t capacity_; // 当前容量
public:
// ... 其他成员函数 ...
void push_back(const T& value) {
if (size_ == capacity_) {
// 如果容量不足,则重新分配内存
reserve(capacity_ == 0 ? 1 : capacity_ * 2);
}
// 在末尾添加新元素
new (data_ + size_) T(value); // 调用元素的拷贝构造函数
++size_;
}
void reserve(size_t new_capacity) {
if (new_capacity > capacity_) {
T* new_data = static_cast<T*>(::operator new(new_capacity * sizeof(T)));
// 将现有元素移动或复制到新内存位置
for (size_t i = 0; i < size_; ++i) {
new (new_data + i) T(std::move_if_noexcept(data_[i]));
data_[i].~T(); // 显式调用析构函数
}
::operator delete(data_);
data_ = new_data;
capacity_ = new_capacity;
}
}
// ... 其他成员函数 ...
};
我们看到push_back()在插入一个元素时,主要做了以下几步操作:
- 检查容量:
push_back()首先会检查当前容量是否足够。如果不够,则调用reserve()函数重新分配内存。 - 重新分配内存:
reserve()函数负责分配新的内存,并将现有元素移动或复制到新位置。注意,这里使用了std::move_if_noexcept()来优化移动操作 - 添加新元素:在容量足够的情况下,
push_back()使用new()在末尾添加新元素,并调用元素的拷贝构造函数。 - 更新大小:最后,
push_back()更新容量的大小。
接下来我们看一下emplace_back()的内部实现,同样用一段简化版的emplace_back()模板函数做演示:
template <typename T>
class vector {
private:
T* data_; // 指向元素的指针
size_t size_; // 当前元素数量
size_t capacity_; // 当前容量
public:
// ... 其他成员函数 ...
template <typename... Args>
void emplace_back(Args&&... args) {
if (size_ == capacity_) {
// 如果容量不足,则重新分配内存
reserve(capacity_ == 0 ? 1 : capacity_ * 2);
}
// 在末尾直接构造新元素,使用传入的参数
new (data_ + size_) T(std::forward<Args>(args)...);
++size_;
}
void reserve(size_t new_capacity) {
if (new_capacity > capacity_) {
T* new_data = static_cast<T*>(::operator new(new_capacity * sizeof(T)));
// 将现有元素移动或复制到新内存位置
for (size_t i = 0; i < size_; ++i) {
new (new_data + i) T(std::move_if_noexcept(data_[i]));
data_[i].~T(); // 显式调用析构函数
}
::operator delete(data_);
data_ = new_data;
capacity_ = new_capacity;
}
}
//其他成员函数 ...
};
我们看到emplace_back()在插入一个元素时,主要做了以下几步操作:
- 检查容量:
emplace_back()首先会检查当前容量是否足够。如果不够,则调用reserve()函数重新分配内存。 - 重新分配内存:
reserve()函数负责分配新的内存,并将现有元素移动或复制到新位置。注意,这里使用了std::move_if_noexcept()来优化移动操作 - 直接构造新元素:在容量足够的情况下,
emplace_back()使用 new 在末尾直接构造新元素, 这避免了不必要的拷贝和移动操作,提高了性能。 - 更新大小:最后,
emplace_back()更新容量的大小。
前面四点其实跟push_back()的差不多,关键的第5点:
- 对比两个函数的实现我们可以看到,
emplace_back()在插入元素时,使用了T(std::forward<Args>(args)...)创建对象,而push_back()则使用了T(value), 那么它们有什么区别呢(以下解释来自混元AI的解释)?
T(std::forward(args)…) 和 T(value)的区别
T(std::forward(args)…) 和 T(value) 是 C++ 中两种不同的对象构造方式,它们在处理参数时有着本质的区别。
T(std::forward(args)…)
这种方式通常出现在模板函数中,特别是像 std::vector 的 emplace_back() 这样的成员函数。这里的 Args 是一个模板参数包,代表传入函数的所有参数类型,而 args 是对应的参数值包。
std::forward(args)… 是一个完美转发(perfect forwarding)的表达式,它会将传入的参数以原始的值类别(左值或右值)传递给 T 的构造函数。 这种方式允许 emplace_back 直接在容器内部构造对象,无需先在外部构造临时对象再移动或复制到容器中,从而提高了效率。 使用 std::forward 可以保持参数的原始值类别,对于右值引用参数,这意味着可以利用移动语义,进一步提高性能。 T(value)
这种方式是在已知具体值的情况下直接构造对象。
-
value 是一个具体的值,可以是左值或右值,但在使用 T(value) 构造对象时,总是会进行一次拷贝或移动操作(取决于 value 的值类别和 T 的构造函数)。
-
如果 value 是一个右值,并且 T 的构造函数支持移动语义,那么会使用移动构造函数;否则,会使用拷贝构造函数。
-
在 push_back 的情况下,如果传入的是一个右值,现代 C++ 标准库实现通常会使用移动语义来避免不必要的拷贝,但这仍然涉及到一次移动操作。yi
五、总结
通过以上介绍,我们相信我们对STL中vector的操作函数push_back()和emplace_back()有了更深入的了解。push_back()和emplace_back()都是向vector容器尾端插入元素的方法,但是它们在内部实现上存在一些差异,从而影响了它们的性能。push_back()在插入元素时,会先调用元素的拷贝构造函数,然后再调用移动构造函数,将元素插入到容器尾部。而emplace_back()则直接调用构造构造函数,直接在容器尾部构造元素,而无需再调用移动构造函数或拷贝构造函数。这种机制对于复杂对象的操作,能更好地优化性能,降低资源的消耗。同时,我们也可以通过push_back()和emplace_back()的源码,进一步了解STL中容器内部实现的一些细节和技巧, 并且知道了其内部实现中 T(std::forward<Args>(args)...)和 T(value)的不同。T(std::forward<Args>(args)...) 是一种更通用的构造方式,它可以在不知道具体参数类型的情况下直接在目标位置构造对象,避免了额外的拷贝或移动操作。T(value) 是一种更具体的构造方式,它需要一个已经确定的值来构造对象,可能会涉及到拷贝或移动操作。在 emplace_back() 和 push_back() 的对比中,emplace_back() 通过使用 std::forward 提供了更高的效率和灵活性。