1. 为什么需要emplace_back?

在C++11之前,我们向vector添加元素主要依赖push_back方法。但这种方式存在一个明显的性能问题:它需要先构造临时对象,再将这个对象拷贝或移动到容器中。举个例子,当你写下vec.push_back(MyClass("test"))时,编译器实际上执行了以下操作:

  1. 调用构造函数创建临时MyClass对象
  2. 调用拷贝/移动构造函数将临时对象放入容器
  3. 销毁临时对象

这种模式在操作复杂对象时会产生不必要的性能开销。我曾经在一个处理3D模型的程序中遇到过这种情况:每个模型对象包含大量顶点数据,使用push_back导致程序运行速度比预期慢了近30%。

emplace_back的诞生正是为了解决这个问题。它允许我们直接在容器内存中构造对象,省去了临时对象的创建和销毁过程。这种技术被称为"就地构造"(in-place construction),是C++11引入的最重要的优化手段之一。

2. emplace_back的工作原理

2.1 完美转发机制

emplace_back的核心在于使用了C++11的完美转发(perfect forwarding)技术。它的函数签名通常是这样的:

template<class... Args>
void emplace_back(Args&&... args);

这里的Args&&是通用引用(universal reference),可以接受任意类型的参数。当调用vec.emplace_back(a, b, c)时,这些参数会被完美转发给元素的构造函数。

我做过一个实验来验证这一点。定义一个简单的类:

class Logger {
public:
    Logger(int x, double y) {
        cout << "构造Logger: " << x << ", " << y << endl;
    }
    Logger(const Logger&) { cout << "拷贝构造" << endl; }
    Logger(Logger&&) { cout << "移动构造" << endl; }
};

然后分别用push_back和emplace_back添加元素:

vector<Logger> vec;
vec.reserve(10);

cout << "使用push_back:" << endl;
vec.push_back(Logger(1, 2.0));

cout << "\n使用emplace_back:" << endl;
vec.emplace_back(3, 4.0);

输出结果清楚地展示了区别:

使用push_back:
构造Logger: 1, 2.0
移动构造

使用emplace_back:
构造Logger: 3, 4.0

2.2 内存分配策略

emplace_back和push_back在内存分配策略上是相同的。当vector容量不足时,两者都会触发重新分配内存。但emplace_back在已有容量下的操作更高效:

  1. push_back流程:

    • 在栈上构造临时对象
    • 将临时对象移动/拷贝到vector内存
    • 销毁临时对象
  2. emplace_back流程:

    • 直接在vector内存构造对象

这种差异在处理大型对象时尤为明显。我曾经测试过一个包含1MB数据的类,emplace_back比push_back快了约15%。

3. 性能对比实测

3.1 基础类型测试

对于基本类型如int,两者性能几乎无差别:

vector<int> intVec;
intVec.reserve(1000000);

auto start = chrono::high_resolution_clock::now();
for(int i=0; i<1000000; ++i) {
    intVec.push_back(i);
}
auto end = chrono::high_resolution_clock::now();
cout << "push_back耗时: " << chrono::duration_cast<chrono::milliseconds>(end-start).count() << "ms\n";

intVec.clear();

start = chrono::high_resolution_clock::now();
for(int i=0; i<1000000; ++i) {
    intVec.emplace_back(i);
}
end = chrono::high_resolution_clock::now();
cout << "emplace_back耗时: " << chrono::duration_cast<chrono::milliseconds>(end-start).count() << "ms\n";

在我的机器上,两者耗时都在15ms左右。

3.2 复杂对象测试

定义一个较复杂的类:

class Complex {
    string name;
    vector<double> data;
public:
    Complex(string n, size_t size) : name(move(n)), data(size) {}
    Complex(const Complex&) = default;
    Complex(Complex&&) = default;
};

测试代码:

vector<Complex> complexVec;
complexVec.reserve(10000);

// push_back测试
auto start = chrono::high_resolution_clock::now();
for(int i=0; i<10000; ++i) {
    complexVec.push_back(Complex("test", 1000));
}
auto end = chrono::high_resolution_clock::now();
cout << "push_back耗时: " << chrono::duration_cast<chrono::milliseconds>(end-start).count() << "ms\n";

complexVec.clear();

// emplace_back测试
start = chrono::high_resolution_clock::now();
for(int i=0; i<10000; ++i) {
    complexVec.emplace_back("test", 1000);
}
end = chrono::high_resolution_clock::now();
cout << "emplace_back耗时: " << chrono::duration_cast<chrono::milliseconds>(end-start).count() << "ms\n";

测试结果显示emplace_back比push_back快了约20%。当数据量更大时,这个差距会更加明显。

4. 使用场景与陷阱

4.1 最佳使用场景

emplace_back在以下情况下特别有用:

  1. 对象构造代价高昂(如分配大量内存)
  2. 对象不可拷贝或移动(但有合适的构造函数)
  3. 需要传递多个参数构造对象
  4. 容器存储的是unique_ptr等只能移动的类型

一个典型例子是构造包含资源的对象:

vector<unique_ptr<Resource>> resources;
resources.emplace_back(make_unique<Resource>(param1, param2));

4.2 需要注意的问题

  1. 参数转发问题:emplace_back会完美转发所有参数,可能导致意外的构造函数调用:
vector<string> strs;
strs.emplace_back(10, 'a');  // 调用string(size_t, char)构造"aaaaaaaaaa"
strs.push_back(10, 'a');     // 编译错误
  1. 异常安全:如果在emplace_back过程中发生异常,可能造成资源泄漏。确保构造函数是异常安全的。

  2. 隐式转换:emplace_back可能触发意外的隐式转换:

vector<string> strs;
strs.emplace_back("hello");  // 正确,调用string(const char*)
strs.push_back("hello");     // 正确,但需要构造临时string对象

struct A { A(int) {} };
vector<A> as;
as.emplace_back(10);  // 正确
as.push_back(10);     // 需要A(int)且可拷贝/移动

5. 深入理解实现细节

5.1 STL中的实现

以libc++的实现为例,emplace_back的核心部分大致如下:

template <class T, class Allocator>
template <class... Args>
void vector<T, Allocator>::emplace_back(Args&&... args) {
    if (this->size() == this->capacity()) {
        // 需要扩容
        this->__push_back_slow_path(std::forward<Args>(args)...);
    } else {
        // 直接在尾部构造
        allocator_traits<allocator_type>::construct(
            this->__alloc_, 
            std::addressof(this->__end_), 
            std::forward<Args>(args)...);
        ++this->__end_;
    }
}

关键点在于使用了allocator_traits的construct方法和完美转发。

5.2 与push_back的底层差异

push_back的典型实现:

void push_back(const value_type& x) {
    if (this->size() == this->capacity()) {
        __push_back_slow_path(x);
    } else {
        allocator_traits<allocator_type>::construct(
            this->__alloc_, 
            std::addressof(*this->__end_), 
            x);
        ++this->__end_;
    }
}

void push_back(value_type&& x) {
    if (this->size() == this->capacity()) {
        __push_back_slow_path(std::move(x));
    } else {
        allocator_traits<allocator_type>::construct(
            this->__alloc_, 
            std::addressof(*this->__end_), 
            std::move(x));
        ++this->__end_;
    }
}

可以看到push_back需要先有对象实例,而emplace_back直接转发参数给构造函数。

6. 现代C++中的最佳实践

6.1 何时使用emplace_back

根据我的经验,以下情况优先使用emplace_back:

  1. 构造参数已知且不需要临时对象时
  2. 对象构造代价较高
  3. 需要避免不必要的拷贝/移动
  4. 使用不可拷贝/移动的类型

6.2 何时使用push_back

以下情况push_back可能更合适:

  1. 已有现成对象需要添加到容器
  2. 代码需要兼容C++11之前的编译器
  3. 需要明确表达"添加已有对象"的语义
  4. 处理简单的内置类型时(性能无差别)

6.3 性能优化技巧

  1. 总是预先reserve足够空间避免重新分配
  2. 对于复杂对象,优先使用emplace_back
  3. 考虑使用移动语义减少拷贝
  4. 批量添加元素时,考虑使用insert或assign

我曾经优化过一个处理大型点云数据的程序,通过将所有的push_back替换为emplace_back并结合reserve,性能提升了约25%。这让我深刻体会到正确使用这些特性的重要性。

更多推荐