Item42:考虑使用置入代替插入
插入函数的隐含性能开销
对于存储 std::string 的标准容器(如 std::vector<std::string>),使用插入函数(push_back/push_front/insert 等)添加元素时,存在隐含的临时对象构造与销毁开销。
示例:
1 | std::vector<std::string> vs; |
- 实参是字符串字面量
const char[6],与容器元素类型std::string不匹配; - 编译器会先创建一个临时
std::string对象(第一次构造); - 临时对象作为右值,传入
push_back(T&&)重载版本,在容器内存中再构造一次元素(移动构造,第二次构造); - 函数返回后,临时对象被销毁(调用析构函数)。
一次插入操作,触发两次构造 + 一次析构,产生不必要的性能损耗。
置入函数的优势:就地构造,无临时对象
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 |
置入函数与插入函数的标准容器对应关系
所有支持插入操作的标准容器,都提供了对应的置入函数:
push_back→emplace_backpush_front→emplace_frontinsert→emplacestd::forward_list::insert_after→emplace_after- 关联容器(
set/map)的带迭代器提示插入 →emplace_hint
置入函数可以完全覆盖插入函数的功能:当传入容器元素类型本身时,置入与插入行为一致。
置入函数性能优于插入函数的三个必要条件
理论上置入函数不会更慢,但实际更快需要同时满足以下条件:
新元素通过构造添加到容器,而非赋值操作
- 基于节点的容器(
list/set/map/unordered_map)始终通过构造添加元素; - 非节点容器(
vector/deque/string)仅emplace_back/emplace_front保证构造; - 若向容器已有元素位置插入(如
vector头部),会触发移动赋值,需创建临时对象,置入优势消失。
- 基于节点的容器(
传入实参类型与容器元素类型不同
置入的价值是避免类型不匹配导致的临时对象,若直接传入容器元素类型,二者无性能差异。
容器不拒绝重复元素(或新增元素大概率不重复)
去重容器(
set/map)的置入实现,会先创建新节点,再判断是否重复;若元素已存在,节点会被直接销毁,构造 + 析构的开销被浪费,此时插入函数性能更优。
置入函数的两大关键风险
1. 异常安全问题:资源泄漏风险
当容器存储智能指针(如 std::shared_ptr)时,直接传入裸指针给置入函数,会引发资源泄漏。
示例:
1 | std::list<std::shared_ptr<Widget>> ptrs; |
- 裸指针
new Widget被完美转发到置入函数; - 若容器分配内存抛出异常,裸指针未被智能指针接管,内存直接泄漏。
安全写法:先创建智能指针,再移动传入
1 | std::shared_ptr<Widget> spw(new Widget, killWidget); |
插入函数天然避免此问题:插入会先创建智能指针临时对象,异常时自动释放资源。
2. 与 explicit 构造函数的交互:隐蔽的未定义行为
置入函数使用直接初始化,允许调用 explicit 构造函数;
插入函数使用拷贝初始化,禁止隐式调用 explicit 构造函数。
以 std::regex 为例:
1 | std::vector<std::regex> regexes; |
emplace_back(nullptr) 虽能编译,但空指针不是合法正则表达式,会触发未定义行为(崩溃、逻辑错误)。
这是置入函数的隐蔽坑:编译器不会拦截 explicit 构造的非法参数,错误更难发现。
总结
- 置入函数性能优于或等于插入函数,无性能劣势;
- 满足构造插入、参数类型不同、容器不查重时,置入函数显著更快;
- 无特殊风险时,优先选用置入函数;存在去重、智能指针裸传、
explicit构造风险时,改用插入函数更安全。
