插入函数的隐含性能开销

对于存储 std::string 的标准容器(如 std::vector<std::string>),使用插入函数(push_back/push_front/insert 等)添加元素时,存在隐含的临时对象构造与销毁开销。

示例:

1
2
std::vector<std::string> vs;
vs.push_back("xyzzy");
  1. 实参是字符串字面量 const char[6],与容器元素类型 std::string 不匹配;
  2. 编译器会先创建一个临时 std::string 对象(第一次构造);
  3. 临时对象作为右值,传入 push_back(T&&) 重载版本,在容器内存中再构造一次元素(移动构造,第二次构造);
  4. 函数返回后,临时对象被销毁(调用析构函数)。

一次插入操作,触发两次构造 + 一次析构,产生不必要的性能损耗。

置入函数的优势:就地构造,无临时对象

C++11 引入的置入函数(emplace_back/emplace_front/emplace 等),基于完美转发实现:

直接将传入的实参转发给容器元素的构造函数,在容器内存中就地构造元素,完全避免临时对象的创建与销毁。

优化为置入函数:

1
vs.emplace_back("xyzzy");
  • 仅执行一次构造,直接在 vector 内部生成 std::string;
  • 无临时对象,无额外析构开销,性能最优。

置入函数支持任意合法的构造参数,例如传入 std::string 的多参数构造函数:

1
vs.emplace_back(50, 'x'); // 直接构造50个'x'组成的string

置入函数与插入函数的标准容器对应关系

所有支持插入操作的标准容器,都提供了对应的置入函数:

  1. push_back → emplace_back
  2. push_front → emplace_front
  3. insert → emplace
  4. std::forward_list::insert_after → emplace_after
  5. 关联容器(set/map)的带迭代器提示插入 → emplace_hint

置入函数可以完全覆盖插入函数的功能:当传入容器元素类型本身时,置入与插入行为一致。

置入函数性能优于插入函数的三个必要条件

理论上置入函数不会更慢,但实际更快需要同时满足以下条件:

  1. 新元素通过构造添加到容器,而非赋值操作

    • 基于节点的容器(list/set/map/unordered_map)始终通过构造添加元素;
    • 非节点容器(vector/deque/string)仅 emplace_back/emplace_front 保证构造;
    • 若向容器已有元素位置插入(如 vector 头部),会触发移动赋值,需创建临时对象,置入优势消失。
  2. 传入实参类型与容器元素类型不同

    置入的价值是避免类型不匹配导致的临时对象,若直接传入容器元素类型,二者无性能差异。

  3. 容器不拒绝重复元素(或新增元素大概率不重复)

    去重容器(set/map)的置入实现,会先创建新节点,再判断是否重复;

    若元素已存在,节点会被直接销毁,构造 + 析构的开销被浪费,此时插入函数性能更优。

置入函数的两大关键风险

1. 异常安全问题:资源泄漏风险

当容器存储智能指针(如 std::shared_ptr)时,直接传入裸指针给置入函数,会引发资源泄漏。

示例:

1
2
3
std::list<std::shared_ptr<Widget>> ptrs;
// 危险写法
ptrs.emplace_back(new Widget, killWidget);
  • 裸指针 new Widget 被完美转发到置入函数;
  • 若容器分配内存抛出异常,裸指针未被智能指针接管,内存直接泄漏。

安全写法:先创建智能指针,再移动传入

1
2
std::shared_ptr<Widget> spw(new Widget, killWidget);
ptrs.emplace_back(std::move(spw));

插入函数天然避免此问题:插入会先创建智能指针临时对象,异常时自动释放资源。

2. 与 explicit 构造函数的交互:隐蔽的未定义行为

置入函数使用直接初始化,允许调用 explicit 构造函数;

插入函数使用拷贝初始化,禁止隐式调用 explicit 构造函数。

以 std::regex 为例:

1
2
3
std::vector<std::regex> regexes;
regexes.emplace_back(nullptr); // 编译通过!直接初始化调用explicit构造函数
regexes.push_back(nullptr); // 编译报错!拷贝初始化禁止隐式转换

emplace_back(nullptr) 虽能编译,但空指针不是合法正则表达式,会触发未定义行为(崩溃、逻辑错误)。

这是置入函数的隐蔽坑:编译器不会拦截 explicit 构造的非法参数,错误更难发现。

总结

  1. 置入函数性能优于或等于插入函数,无性能劣势;
  2. 满足构造插入、参数类型不同、容器不查重时,置入函数显著更快;
  3. 无特殊风险时,优先选用置入函数;存在去重、智能指针裸传、explicit 构造风险时,改用插入函数更安全。