emplace_back vs push_back 详解(面试标准版)
一、一句话定位
push_back把一个已经构造好的对象 拷贝/移动进容器;emplace_back把构造参数完美转发 到容器内存上原地构造,省去中间临时对象。
cpp
struct Person {
Person(string n, int a) : name(std::move(n)), age(a) {}
string name; int age;
};
vector<Person> v;
v.push_back(Person("Tom", 18)); // ①先构造临时Person → ②移动进容器(临时对象销毁)
v.emplace_back("Tom", 18); // 直接在容器内存上用("Tom",18)构造 → 无临时对象
二、执行流程对比图
ini
═══ push_back(Person("Tom",18)) ═══ ═══ emplace_back("Tom",18) ═══
堆上构造临时对象 容器扩容后的目标内存
┌─────────────┐ ┌─────────────┐
│ name="Tom" │ │ │
│ age = 18 │ │ │
└─────────────┘ │ │
│ 移动构造(Move) ▲ 直接在这里调用
▼ │ Person("Tom",18)
容器内存 ┌─────────────┐ │
│ name="Tom" │ ◄────────────────────┘
│ age = 18 │
└─────────────┘ ┌─────────────┐
临时对象析构 💀 │ name="Tom" │
│ age = 18 │
总计: 构造1 + 移动1 + 析构1 └─────────────┘
总计: 构造1 ✅ 完事
三种重载的完整真相(push_back 其实也有移动版本):
cpp
push_back(const T&); // C++03: 拷贝版 → lvalue走这里
push_back(T&&); // C++11: 移动版 → 右值临时对象走这里
emplace_back(Args&&...); // 万能引用+完美转发,任意参数
所以对右值临时对象 ,push_back 的开销 = 1次构造 + 1次移动;emplace_back = 1次构造。差距只有那一次移动(对string/vector这类有堆资源的对象,移动虽便宜但不是零成本;对纯POD则几乎无差别)。
三、emplace_back 的真正优势场景(不只是省移动)
场景1:构造函数本身有开销时,省掉整个临时对象
cpp
// 构造函数内部做重量级工作(如解析、分配)
v.push_back(BigData("very long string...", 1000, config));
// └─ 先完整构造临时对象,再移动
v.emplace_back("very long string...", 1000, config);
// └─ 参数直接转发,只构造一次,临时对象从未存在
场景2:禁止移动/拷贝的类型,push_back 根本用不了
cpp
class Widget {
public:
Widget(int x);
Widget(const Widget&) = delete;
Widget(Widget&&) = delete; // 不可拷贝不可移动!
};
vector<Widget> v;
v.push_back(Widget(42)); // ❌ 编译错误:需要移动
v.emplace_back(42); // ✅ 唯一选择:原地构造
// 这就是 mutex、atomic、lock_guard 等类型进容器的唯一方式
场景3:隐式转换行为的差异(隐藏陷阱⚠️)
cpp
vector<string> vs;
vs.push_back("hello"); // ✅ 安全:隐式构造临时string再移动
// push_back参数类型固定是string,不会意外调错
vs.emplace_back("hello"); // ✅ 也行,但...
vs.emplace_back(42); // ❌ 危险! 完美转发会匹配string(int)的
// explicit构造? 不,explicit挡住了→编译错
// 但如果构造函数非explicit就静默构造了!
vs.emplace_back(nullptr); // ⚠️ 匹配到 string(const char*)? nullptr是
// nullptr_t → 编译错,但某些类型可能
// 匹配到意想不到的重载
要点 :
explicit构造函数在 emplace_back 的直接初始化语境下会被调用 (emplace_back内部是直接初始化),而push_back(T&&)拷贝初始化语义下 explicit 不参与隐式转换------所以 push_back 的类型检查更严格、更安全;emplace_back 更高效但更"放得开"。
四、对比总结表
| 维度 | push_back | emplace_back |
|---|---|---|
| 来源 | C++03(右值版C++11) | C++11 |
| 参数 | 已构造的 const T& / T&& |
构造参数包 Args&&... |
| 机制 | 拷贝/移动已有对象 | 完美转发参数,容器内存上原地构造 |
| 开销(右值入容器) | 1构造 + 1移动 | 1构造 |
| 开销(lvalue入容器) | 1拷贝 | 同样1拷贝(无优势!) |
| 支持不可移动类型 | ❌ | ✅(mutex等唯一途径) |
| 类型安全 | 严格(固定T类型) | 较宽松(explicit直接初始化可命中) |
| 扩容时的行为 | 相同:都需要移动已有元素,都可能触发allocator | 相同 |
| 关键澄清(防杠点): |
传 lvalue 时两者一样都是拷贝,emplace_back 没有任何优势:
cppPerson p("Tom", 18); v.push_back(p); // 拷贝 v.emplace_back(p); // 也是拷贝! 别以为emplace就快元素是 int/double等POD 时两者差异可忽略;
有移动构造 的类,差距只有一次廉价移动------性能收益经常被高估;
真正不可替代的是:不可移动类型 + 构造参数转发。
五、底层实现原理(追问必备)
cpp
// emplace_back 的核心:allocator 的 construct + 完美转发
template <typename... Args>
reference emplace_back(Args&&... args) {
if (size_ == capacity_) realloc(); // 扩容逻辑完全一样
alloc_.construct(data_ + size_, // 在这块原始内存上
std::forward<Args>(args)...); // 转发参数构造
return *data_ + size_++;
// └─ 完美转发: 左值参数保持左值,右值保持右值,
// 最终调用 T(Args...) 匹配最合适的构造函数
}
关键点:容器内存是未初始化的原始内存 ,
push_back用"构造临时+移动/拷贝"两步;emplace_back用 placement new 直接一步构造。扩容行为两者完全一致。
六、高频追问速答
| 追问 | 答案 |
|---|---|
| emplace_back 一定比 push_back 快吗? | 不一定。lvalue时相同;有移动构造时只省一次廉价移动;真正收益在无移动类型和重构造场景。可读性优先时push_back更明确 |
| 为什么emplace_back能绕过移动? | 它拿到的是构造参数而非对象,用placement new在容器内存直接调构造函数 |
| 完美转发失败的情况? | 传NULL/位域/重载歧义参数时,转发后可能匹配到意外构造函数;{1,2}初始化列表不能转发------v.emplace_back({1,2})编译错,要用v.push_back({1,2}) |
| emplace_back返回什么? | C++17起返回新元素的引用 ,可以链式操作:v.emplace_back(x).doSomething() |
| insert 和它们的关系? | insert在任意位置 插入(版本同push_back),C++17也有emplace对应版emplace(pos, args...) |
| 什么时候坚持用push_back? | ①想要显式类型转换检查 ②传初始化列表 ③代码意图清晰:push_back(make_...()) 一眼看懂对象来源 |
七、30秒总结话术(背诵版)
"push_back接收已构造对象,右值走移动版;emplace_back用万能引用接收构造参数包,完美转发后在容器内存上placement new原地构造,省掉临时对象的构造+移动。三者差异:右值入容器时emplace省一次移动;不可拷贝不可移动的类型如mutex只能用emplace ;lvalue传入时两者都是拷贝、无差别。注意emplace_back因为走直接初始化,explicit构造函数也会被匹配,类型安全不如push_back严格,而且初始化列表参数只能用push_back。所以我的习惯是:默认push_back语义清晰,需要参数转发或类型不可移动时用emplace_back,而不是无脑全用emplace。"