C++ 核心指南之资源管理(下)智能指针最佳实践
C++ 核心指南(C++ Core Guidelines)是由 Bjarne Stroustrup、Herb Sutter 等顶尖 C+ 专家创建的一份 C++ 指南、规则及最佳实践。旨在帮助大家正确、高效地使用“现代 C++”。
这份指南侧重于接口、资源管理、内存管理、并发等 High-level 主题。遵循这些规则可以最大程度地保证静态类型安全,避免资源泄露及常见的错误,使得程序运行得更快、更好。
R.smart:智能指针
- R.20:使用 unique_ptr 或 shared_ptr 来表示所有权
- R.21:除非需要共享所有权,否则优先使用 unique_ptr 而不是 shared_ptr
- R.22:使用 make_shared() 创建 shared_ptr
- R.23:使用 make_unique() 创建 unique_ptr
- R.24:使用 std::weak_ptr 打破 shared_ptr 的循环引用
- R.30: 仅在需要明确表示生命周期语义时才将智能指针作为参数传递
- R.31: 如果你使用的是非 std 智能指针,遵循 std 的基本模式
- R.32: 将形参声明为 unique_ptr
来表明函数对 widget 的所有权 - R.33: 将形参声明为 unique_ptr
& 来表明函数对 widget 的重新赋值语义 - R.34: 将形参声明为 shared_ptr
来明确表明函数共享所有权 - R.35: 将形参声明为 shared_ptr
& 来表明函数可能重新赋值共享指针 - R.36: 将形参声明为 const shared_ptr
& 来表明它可能增加对象的引用计数 - R.37: 不要传递从“智能指针别名”取得的指针或引用
R.20:使用 unique_ptr 或 shared_ptr 来表示所有权
可以避免资源泄露
1 | void f() |
代码检查
如果 new 的结果赋给了裸指针,给出警告
如果函数返回的“拥有指针”赋给了裸指针,给出警告
R.21:除非需要共享所有权,否则优先使用 unique_ptr 而不是 shared_ptr
unique_ptr 在概念上更简单、可预测(知道何时发生析构),而且更快(无需隐式维护使用计数)。
反面例子
增加和维护了一个不必要的引用计数
1 | void f() |
例子
1 | void f() |
代码检查
如果一个函数使用了一个在函数内部分配的 shared_ptr,但从不返回该 shared_ptr 或将其传递给形参为 shared_ptr& 的函数,则给出警告。建议使用 unique_ptr 替代。
R.22:使用 make_shared() 创建 shared_ptr
make_shared 提供了更简洁的构造语句。而且 make_shared 有机会将引用计数存储在其关联对象的相邻位置。
示例
1 | shared_ptr<X> p1 { new X{2} }; // BAD |
使用 make_shared() 版本只提到了 X 一次,所以它通常比使用显式 new 的版本更短,同时也更快。
代码检查
如果一个 shared_ptr 是由 new 的结果构造而不是 make_shared,则给出警告。
R.23:使用 make_unique() 创建 unique_ptr
make_unique 提供了更简洁的构造语句。它还确保在复杂表达式中的异常安全。
make_unique 是 C++14 引入的,而 make_shared 在 C++11 就已经有了
示例
1 | // 可行,但重复出现 Foo |
代码检查
如果一个 unique_ptr 是由 new 的结果构造而不是 make_unique,则给出警告。
R.24:使用 std::weak_ptr 打破 shared_ptr 的循环引用
shared_ptr 依赖于引用计数,而循环结构的引用计数永远不为 0,因此我们需要一种机制来打破循环结构。
例子
1 | #include <memory> |
Herb Sutter:有很多人说“打破循环引用”,但我认为“临时共享所有权”更准确。
Bjarne Stroustrup:“打破循环”是必须要做的,“临时共享所有权”是你如何“打破循环”。你可以通过使用另一个 shared_ptr 来“临时共享所有权”。(这里不太好翻译,贴出原文:breaking cycles is what you must do; temporarily sharing ownership is how you do it. You could “temporarily share ownership” simply by using another shared_ptr)
代码检查
如果可以静态地检测到循环(可能无法实现),就不需要 weak_ptr。
R.30: 仅在需要明确表示生命周期语义时才将智能指针作为参数传递
参见 F.7: 对于一般用途,使用 T* 或 T& 参数而不是智能指针
R.31: 如果你使用的是非 std 智能指针,遵循 std 的基本模式
任何重载了一元 * 和 -> 运算符的类型(包括模板及特化模板)都被认为是智能指针:
如果它是可复制的,则应被视为 shared_ptr
如果它不可复制,则应被视为 unique_ptr
反面例子
1 | // Boost 的 intrusive_ptr |
p 是一个共享指针,但在这里没有用到它的共享性,并且通过值传递会导致性能下降;函数只有在需要参与 widget 的生命周期管理的时候使用智能指针。否则,应该使用 widget& 或者 widget*(如果实参可能是 nullptr)作为形参。
这些第三方/自定义的智能指针与 std::shared_ptr 概念一致,因此接下来的规则也适用于其他类型的第三方和自定义智能指针。这对于排查常见的智能指针错误、性能问题时非常有用。
R.32: 将形参声明为 unique_ptr 来表明函数对 widget 的所有权
R.33: 将形参声明为 unique_ptr& 来表明函数对 widget 的重新赋值语义
以这种方式使用 unique_ptr 既可以起到 self-documented 的作用,又可以强制执行函数调用的所有权转移或重新赋值(reseat)语义。
注意:“重新赋值”(reseat)的意思是:使指针或智能指针指向不同的对象。
例子
1 | // 接受 widget 的所有权 |
反面例子
1 | // 通常不是你想要的 |
代码检查
如果一个函数通过左值引用接受 unique_ptr
如果一个函数通过 const 引用接受 unique_ptr
R.34: 将形参声明为 shared_ptr 来明确表明函数共享所有权
R.35: 将形参声明为 shared_ptr& 来表明函数可能重新赋值共享指针
R.36: 将形参声明为 const shared_ptr& 来表明它可能增加对象的引用计数
注:“重新赋值”(reseat)的意思是:使引用或智能指针指向不同的对象。
例子
1 | class WidgetUser |
代码检查
如果函数通过左值引用接受 shared_ptr
如果函数通过值传递或 const 引用接受 shared_ptr
如果函数通过右值引用接受 shared_ptr
R.37: 不要传递从“智能指针别名”取得的指针或引用
违反本规则是导致丢失引用计数、产生悬空指针的首要原因。函数应优先考虑传递裸指针或引用到调用链的下游。在调用树的顶部,从智能指针取得裸指针或引用时,需要确保智能指针在调用树内部不会无意中被重置或重新赋值。
注:有时需要对智能指针进行本地拷贝,以保证在函数调用树的整个期间保持对象不被释放。
例子
1 | // 全局(静态或堆),或者本地智能指针的别名 |
下面的代码无法通过 code review
1 | void my_code() |
解决办法:拷贝一份副本,确保函数调用树整个期间引用计数不为 0
1 | void my_code() |
代码检查
如果在函数调用过程中使用的指针或引用是从一个非本地的 shared_ptr 或 unique_ptr 取得,或者从一个本地、但可能是别名的智能指针取得,给出警告。
如果是 shared_ptr,建议保存一个本地副本,然后从该副本获取指针或引用。