本章介绍了两个关于性能微调的建议,主要涉及函数参数传递策略和容器元素插入优化。这些建议并非通用的“银弹”,而是需要在特定场景下权衡使用的技术。
条款四十一:对于移动成本低且总是被拷贝的可拷贝形参,考虑按值传递
在 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次移动)。
适用条件 (必须同时满足):
对象是可拷贝的 (Copyable):如果是只移动类型(如
unique_ptr),直接按值传递也是没必要的,应该用右值引用重载。移动成本低 (Cheap to move):如
std::string、std::vector等。如果是std::array或大对象,多一次移动的开销不可忽视。总是被拷贝 (Always copied):如果在函数内部只有在特定条件下才保存参数,按值传递会导致在不需要保存时也付出了构造/析构的代价。此时应传引用。
不适用的陷阱:
通过赋值拷贝 (Copy via Assignment):如果函数内部是赋值给现有成员变量(如
this->str = std::move(param)),按值传递可能会导致内存重新分配,而无法利用现有对象的容量(Capacity)。在这种情况下,传引用可能快得多。切片问题 (Slicing):基类参数绝不能按值传递,否则派生类部分会被切掉。
条款四十二:考虑使用置入代替插入
C++11 引入了 emplace_back、emplace 等置入函数,旨在消除临时对象的创建和销毁开销。
插入 (Insertion) vs. 置入 (Emplacement):
插入 (
push_back):接受一个对象。如果你传递的不是该类型的对象(如传递字符串字面量给vector<string>),编译器会先创建一个临时对象,再将其移动/拷贝到容器中,最后销毁临时对象。置入 (
emplace_back):接受构造函数的参数。它使用完美转发将参数直接传递给容器内存中元素的构造函数,在原地构造对象。没有临时对象。
什么时候置入比插入更快? (启发式规则,通常需满足以下所有条件):
值是被构造到容器中的:而不是被赋值(例如
vector的末尾添加是构造,中间插入可能是赋值)。传入的参数类型与容器元素类型不同:如果类型相同,
push_back本身也不会产生额外的临时对象转换。容器不拒绝重复值:对于
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)则会在编译时报错。
评论