文章目录

C++ vector 的 emplace_back 为什么能省去临时对象的拷贝构造

发布于 2026-05-28 02:22:00 · 浏览 35 次 · 评论 0 条

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);   // 同上

发生了什么事?

  1. vector 确保有足够容量(若不够则重新分配并移动已有元素)。
  2. 在 vector 内部的新位置上,使用参数 "hello"42 直接调用 MyData(const std::string&, int) 构造函数。
  3. 没有临时对象被创建,因此没有拷贝或移动发生。

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 构造函数
  • 只有一个参数的构造函数:如果类有单参数构造函数且未标记 explicitemplace_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 以减少扩容带来的移动开销。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文