1. 为什么emplace_back比push_back更快?

这个问题困扰过很多C++开发者。我刚开始接触C++11时也纳闷,不就是往容器末尾加个元素吗,能有多大区别?直到有次处理百万级数据时,emplace_back居然比push_back快了近30%,这才让我下定决心要搞懂它们的底层差异。

先看个生活例子:假设你要往书架上放一本书。push_back的做法是先在外面把书印刷好(构造临时对象),再把整本书搬进书架(拷贝/移动);而emplace_back则是直接把印刷机搬进书架,在书架上完成印刷(原地构造)。显然,后者省去了搬运环节,效率自然更高。

从编译器视角看,关键差异在于构造方式。push_back必须接受一个已构造的对象:

std::vector<std::string> vec;
std::string temp("hello");  // 先构造临时对象
vec.push_back(temp);        // 再拷贝/移动

而emplace_back直接传递构造参数:

vec.emplace_back("hello");  // 直接在vector内存构造

在STL源码中(以libstdc++为例),push_back最终会调用_M_realloc_insert进行元素搬迁,而emplace_back通过完美转发(perfect forwarding)将参数直达构造函数。这个优化在对象构造成本高时(比如std::string、自定义类)尤为明显。

2. 深入源码看实现差异

2.1 push_back的"两步走"策略

在GCC的stl_vector.h中,push_back的核心逻辑是这样的:

void push_back(const value_type& __x) {
    if (_M_impl._M_finish != _M_impl._M_end_of_storage) {
        _Alloc_traits::construct(_M_impl, _M_impl._M_finish, __x);
        ++_M_impl._M_finish;
    } else
        _M_realloc_insert(end(), __x);
}

注意这个_Alloc_traits::construct调用,它实际上执行的是placement new,在已分配的内存上构造对象。当vector容量不足时,会触发_M_realloc_insert——这个函数会:

  1. 分配新内存
  2. 拷贝原元素
  3. 构造新元素
  4. 销毁原内存中的对象

2.2 emplace_back的"一站式"构造

再看emplace_back的实现:

template<typename... _Args>
void emplace_back(_Args&&... __args) {
    if (_M_impl._M_finish != _M_impl._M_end_of_storage) {
        _Alloc_traits::construct(_M_impl, _M_impl._M_finish,
                               std::forward<_Args>(__args)...);
        ++_M_impl._M_finish;
    } else
        _M_realloc_insert(end(), std::forward<_Args>(__args)...);
}

关键区别在于std::forward<_Args>(__args)...这个参数包展开。它通过完美转发将参数原封不动地传递给构造函数,避免了临时对象的产生。举个例子:

class Person {
public:
    Person(string&& name, int age) {...}
};

vector<Person> people;
people.emplace_back("Alice", 30);  // 直接转发参数给构造函数

3. 性能对比实测数据

为了量化两者的差异,我设计了三个测试场景:

3.1 基础类型测试

对int类型进行1000万次插入:

  • push_back: 28ms
  • emplace_back: 27ms

基本无差异,因为int的构造/拷贝成本极低。

3.2 字符串测试

构造包含100万个随机字符串(长度50-100)的vector:

方法耗时(ms)内存峰值(MB)
push_back420112
emplace_back38098

emplace_back节省了约10%的时间和内存,因为避免了string的临时构造和移动。

3.3 复杂对象测试

自定义类包含5个string成员和1个vector:

class ComplexObj {
    string a, b, c, d, e;
    vector<int> nums;
public:
    ComplexObj(string a, string b, string c, string d, string e, initializer_list<int> il)
        : a(a), b(b), c(c), d(d), e(e), nums(il) {}
};

测试结果(10万次插入):

方法耗时(ms)拷贝构造函数调用次数
push_back1250100,000
emplace_back8600

性能差距拉大到30%以上,因为避免了所有临时对象的构造和移动。

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

4.1 何时该用emplace_back?

  1. 构造参数较多时:
// 更优
vec.emplace_back(arg1, arg2, arg3); 

// 较差
vec.push_back(MyClass(arg1, arg2, arg3));
  1. 容器元素类型不可拷贝时:
class NonCopyable {
    NonCopyable(const NonCopyable&) = delete;
public:
    NonCopyable(int x) {...}
};

vector<NonCopyable> v;
v.emplace_back(42);  // 唯一可行的方式

4.2 需要小心的陷阱

  1. 隐式转换问题:
vector<string> strs;
strs.emplace_back(50, 'a');  // 构造string(50, 'a')
strs.push_back(50, 'a');     // 编译错误,参数不匹配
  1. 资源管理类需要特别注意:
vector<shared_ptr<Foo>> ptrs;
ptrs.emplace_back(new Foo);  // 可能内存泄漏
ptrs.push_back(make_shared<Foo>());  // 更安全

4.3 C++17/20的改进

C++17引入的std::string_view配合emplace_back更高效:

vector<string> strs;
string_view sv = "hello world";
strs.emplace_back(sv);  // 避免字符串拷贝

C++20的concepts可以约束模板参数:

template<typename... Args>
requires std::constructible_from<T, Args...>
void emplace_back(Args&&... args);

5. 编译器优化的影响

现代编译器(GCC10+/Clang12+/MSVC2019+)对push_back有特殊优化。例如这段代码:

vec.push_back(Foo{arg1, arg2});

优化后的汇编代码可能与emplace_back几乎相同,因为编译器能识别出临时对象的构造可以省略。但以下情况优化会失效:

  1. 跨翻译单元调用
  2. 涉及虚函数调用
  3. 构造参数有副作用

通过Godbolt编译器资源管理器可以看到,在-O3优化下,简单场景两者的汇编代码差异确实会缩小,但在复杂对象场景仍保持明显区别。

6. 实际项目中的经验

在数据库引擎开发中,我们曾用emplace_back重构元数据管理模块。原代码:

void addColumn(const string& name, SQLType type) {
    columns_.push_back(ColumnMeta{name, type});
}

重构后:

void addColumn(string_view name, SQLType type) {
    columns_.emplace_back(name, type);
}

性能提升来自三方面:

  1. 消除ColumnMeta临时对象
  2. 改用string_view避免字符串拷贝
  3. 减少一次内存分配(原临时对象的内存)

最终该变更使表创建操作提速15%,内存占用降低8%。这告诉我们,性能优化往往来自多个小改进的累积。

理解emplace_back的优势后,现在我在代码审查时会特别关注push_back的使用场景。当看到构造临时对象+push_back的组合时,就会建议改用emplace_back。这种改变虽然微小,但在高频执行的代码路径上能产生显著收益。

更多推荐