本章介绍了两个关于性能微调的建议,主要涉及函数参数传递策略和容器元素插入优化。这些建议并非通用的“银弹”,而是需要在特定场景下权衡使用的技术。

条款四十一:对于移动成本低且总是被拷贝的可拷贝形参,考虑按值传递

在 C++98 中,避免按值传递用户自定义类型是金科玉律。但在 C++11 中,对于特定情况,按值传递成为了一种既简单又相对高效的策略。

  • 场景:你需要编写一个函数,该函数接收一个参数,并将其拷贝(复制或移动)保存到内部数据结构中。

  • 替代方案对比

    • 重载 (Overloading):为 const T& (左值) 和 T&& (右值) 分别编写函数。

      • 优点:最高效(左值拷贝,右值移动)。

      • 缺点:代码重复,维护困难。

    • 通用引用 (Universal References):使用模板 T&&std::forward

      • 优点:最高效。

      • 缺点:必须在头文件中实现,错误信息晦涩,可能意外匹配不该匹配的类型。

    • 按值传递 (Pass by Value)void func(T param) { container.push_back(std::move(param)); }

      • 优点:代码简洁,只需一个函数,无需模板。

      • 代价:相比重载或通用引用,多一次移动操作(左值:1次拷贝+1次移动;右值:2次移动)。

  • 适用条件 (必须同时满足)

    1. 对象是可拷贝的 (Copyable):如果是只移动类型(如 unique_ptr),直接按值传递也是没必要的,应该用右值引用重载。

    2. 移动成本低 (Cheap to move):如 std::stringstd::vector 等。如果是 std::array 或大对象,多一次移动的开销不可忽视。

    3. 总是被拷贝 (Always copied):如果在函数内部只有在特定条件下才保存参数,按值传递会导致在不需要保存时也付出了构造/析构的代价。此时应传引用。

  • 不适用的陷阱

    • 通过赋值拷贝 (Copy via Assignment):如果函数内部是赋值给现有成员变量(如 this->str = std::move(param)),按值传递可能会导致内存重新分配,而无法利用现有对象的容量(Capacity)。在这种情况下,传引用可能快得多。

    • 切片问题 (Slicing):基类参数绝不能按值传递,否则派生类部分会被切掉。

条款四十二:考虑使用置入代替插入

C++11 引入了 emplace_backemplace 等置入函数,旨在消除临时对象的创建和销毁开销。

  • 插入 (Insertion) vs. 置入 (Emplacement)

    • 插入 (push_back):接受一个对象。如果你传递的不是该类型的对象(如传递字符串字面量给 vector<string>),编译器会先创建一个临时对象,再将其移动/拷贝到容器中,最后销毁临时对象。

    • 置入 (emplace_back):接受构造函数的参数。它使用完美转发将参数直接传递给容器内存中元素的构造函数,在原地构造对象。没有临时对象

  • 什么时候置入比插入更快? (启发式规则,通常需满足以下所有条件):

    1. 值是被构造到容器中的:而不是被赋值(例如 vector 的末尾添加是构造,中间插入可能是赋值)。

    2. 传入的参数类型与容器元素类型不同:如果类型相同,push_back 本身也不会产生额外的临时对象转换。

    3. 容器不拒绝重复值:对于 set/map,为了检查值是否存在,实现通常必须先创建一个节点(包含临时对象)来进行比较,这抵消了置入的优势。

  • 置入的陷阱

    • 异常安全 (Resource Management)

      • ptrs.push_back(std::shared_ptr<Widget>(new Widget, del)):先构造智能指针,再调用 push_back。异常安全。

      • ptrs.emplace_back(new Widget, del)new Widget 先执行返回裸指针,如果在 emplace_back 内部申请容器节点内存失败抛出异常,裸指针会泄漏

      • 建议:对于资源管理对象,先创建好智能指针,再传递给容器(此时置入和插入无区别)。

    • explicit 构造函数的绕过

      • 插入函数使用拷贝初始化(Copy Initialization),不能调用 explicit 构造函数。

      • 置入函数使用直接初始化(Direct Initialization),可以调用 explicit 构造函数。

      • 例子vector<regex> re; re.emplace_back(nullptr); 可以编译通过(因为 regex 有个接受指针的 explicit 构造函数),但这会导致运行时崩溃。re.push_back(nullptr) 则会在编译时报错。