C++ vector 的 emplace_back 为什么能省去临时对象的拷贝构造
在 C++ 中,向 std::vector 尾部添加新元素时,传统做法是使用 push_back。但现代 C++ 推荐使用 emplace_back,因为它可以避免临时对象的构造和拷贝。理解其原理,能帮助你写出更高效的代码。
1. 回顾 push_back 的工作流程
push_back 接收一个已经存在的对象,并将其拷贝或移动到 vector 内部。
#include <vector>
#include <string>
struct MyData {
std::string name;
int value;
MyData(const std::string& n, int v) : name(n), value(v) {}
};
int main() {
std::vector<MyData> vec;
// 方式一:先构造临时对象,再拷贝到 vector 中
vec.push_back(MyData("hello", 42));
// 方式二:先构造临时对象,再移动(如果移动构造函数存在)
vec.push_back(std::move(MyData("world", 10)));
return 0;
}
临时对象产生的开销:
- 方式一:
MyData("hello", 42)在栈上构造了一个临时对象 → 调用拷贝构造函数将其复制到 vector 内部 → 临时对象析构。 - 方式二:临时对象构造 → 调用移动构造函数转移资源 → 临时对象析构(移动通常比拷贝快,但仍有一个临时对象的额外生命周期)。
2. emplace_back 如何省去临时对象
emplace_back 是变参模板函数。它接收的参数直接转发给 MyData 的构造函数,在 vector 已分配的内存空间中原地构造对象,全程没有临时对象出现。
vec.emplace_back("hello", 42); // 直接在 vector 中构造 MyData("hello",42)
vec.emplace_back("world", 10); // 同上
发生了什么事?
- vector 确保有足够容量(若不够则重新分配并移动已有元素)。
- 在 vector 内部的新位置上,使用参数
"hello"和42直接调用MyData(const std::string&, int)构造函数。 - 没有临时对象被创建,因此没有拷贝或移动发生。
3. 代码对比:显式观察构造/析构调用
为了更直观地看出区别,我们给 MyData 添加打印语句。
#include <iostream>
#include <vector>
struct MyData {
std::string name;
int value;
MyData(const std::string& n, int v) : name(n), value(v) {
std::cout << "Constructed: " << name << "\n";
}
MyData(const MyData& other) : name(other.name), value(other.value) {
std::cout << "Copied: " << name << "\n";
}
MyData(MyData&& other) noexcept : name(std::move(other.name)), value(other.value) {
std::cout << "Moved: " << name << "\n";
}
~MyData() {
std::cout << "Destructed: " << name << "\n";
}
};
int main() {
std::vector<MyData> vec;
std::cout << "--- push_back with temporary ---\n";
vec.push_back(MyData("A", 1));
std::cout << "--- emplace_back ---\n";
vec.emplace_back("B", 2);
return 0;
}
可能的输出(实际顺序可能因编译器优化略有不同,但本质一致):
--- push_back with temporary ---
Constructed: A // 临时对象构造
Copied: A // 拷贝到 vector 内部
Destructed: A // 临时对象析构
--- emplace_back ---
Constructed: B // 直接在 vector 内部构造,无拷贝、无析构
Destructed: A // vector 析构时销毁两个元素
Destructed: B
可以看到,push_back 多了一次构造 + 一次拷贝 + 一次析构。而 emplace_back 只有一次构造。
4. 深入原理:完美转发与就地构造
emplace_back 的核心是完美转发(perfect forwarding):
template<typename... Args>
reference emplace_back(Args&&... args); // 伪代码示意
- 参数以
Args&&形式被接收,保持原始值类别(左值/右值)。 - 在 vector 内部,使用
::new ((void*)ptr) T(std::forward<Args>(args)...)在未初始化的内存上调用构造函数。 - 这相当于在目标地址直接调用构造函数,没有经过任何中间变量。
对比 push_back 的简化实现:
void push_back(const T& value) {
// ... 确保容量
::new ((void*)ptr) T(value); // 调用拷贝构造函数
}
void push_back(T&& value) {
// ...
::new ((void*)ptr) T(std::move(value)); // 调用移动构造函数
}
无论哪种重载,都会先构造一个 T 类型的对象(无论是临时对象还是具名对象),再将其拷贝/移动到容器内。
5. 什么时候必须用 push_back?
虽然 emplace_back 通常更高效,但有些场景下 push_back 是必要的或更合适:
- 需要显式调用转换构造函数(explicit constructor):如果类的构造函数被标记为
explicit,你不能用emplace_back隐式构造。例如:
struct ExplicitData {
explicit ExplicitData(int x) { /* ... */ }
};
std::vector<ExplicitData> v;
v.emplace_back(42); // 编译错误!explicit 构造函数不能用于隐式转换
v.push_back(ExplicitData(42)); // 正确,显式构造临时对象
v.push_back({42}); // 同样错误,列表初始化也会调用 explicit 构造函数
-
只有一个参数的构造函数:如果类有单参数构造函数且未标记
explicit,emplace_back可能会产生意外的隐式转换。但通常这是设计缺陷,建议将单参数构造函数设为explicit。 -
需要拷贝已存在的对象:如果你手头已经有一个对象,直接
push_back即可:vec.push_back(existingObj);。当然也可以用vec.emplace_back(existingObj);,但此时会退化为拷贝构造(因为existingObj是左值,完美转发后仍是左值),效率并无区别。但为了意图清晰,推荐使用push_back。
6. 性能陷阱:重复分配与移动
emplace_back 避免的是临时对象的构造/拷贝,但无法避免 vector 扩容时已有元素的移动(或拷贝)。当容量不足时,vector 会重新分配内存并将所有元素移动到新内存。这是 vector 本身的设计限制,与 emplace_back 无关。如果你在性能敏感场景频繁添加元素,建议预先 reserve 足够容量。
vec.reserve(1000); // 提前分配,避免多次扩容
for (int i = 0; i < 1000; ++i) {
vec.emplace_back("data", i);
}
7. 总结要点
push_back的工作方式:先构造对象,再拷贝/移动到容器。emplace_back的工作方式:直接在容器内部构造对象,不产生中间临时对象。- 原理:变参模板 + 完美转发 + 定位
new。 - 使用建议:大多数情况下优先使用
emplace_back,尤其是构造参数较多时;但遇到explicit构造函数或已有现成对象时,用push_back更清晰。 - 别忘了
reserve以减少扩容带来的移动开销。

暂无评论,快来抢沙发吧!