

C++ 核心指南之资源管理(下)—— 智能指针最佳实践 - Zijian/TENG
source link: https://www.cnblogs.com/tengzijian/p/17516959.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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>
来表明函数对 widget 的所有权 - R.33: 将形参声明为
unique_ptr<widget>&
来表明函数对 widget 的重新赋值语义 - R.34: 将形参声明为
shared_ptr<widget>
来明确表明函数共享所有权 - R.35: 将形参声明为
shared_ptr<widget>&
来表明函数可能重新赋值共享指针 - R.36: 将形参声明为
const shared_ptr<widget>&
来表明它可能增加对象的引用计数 - R.37: 不要传递从“智能指针别名”取得的指针或引用
R.20:使用 unique_ptr 或 shared_ptr 来表示所有权
可以避免资源泄露
void f()
{
// bad, p1 可能泄露
X* p1 { new X };
// good, 独占所有权
auto p2 = make_unique<X>();
// good, 共享所有权
auto p3 = make_shared<X>();
}
- 如果 new 的结果赋给了裸指针,给出警告
- 如果函数返回的“拥有指针”赋给了裸指针,给出警告
R.21:除非需要共享所有权,否则优先使用 unique_ptr 而不是 shared_ptr
unique_ptr 在概念上更简单、可预测(知道何时发生析构),而且更快(无需隐式维护使用计数)。
增加和维护了一个不必要的引用计数
void f()
{
shared_ptr<Base> base = make_shared<Derived>();
// 在本地使用 base,不需要拷贝,引用计数永远不会超过 1
} // 销毁 base
void f()
{
unique_ptr<Base> base = make_unique<Derived>();
// 在本地使用 base
} // 销毁 base
如果一个函数使用了一个在函数内部分配的 shared_ptr,但从不返回该 shared_ptr 或将其传递给形参为 shared_ptr& 的函数,则给出警告。建议使用 unique_ptr 替代。
R.22:使用 make_shared() 创建 shared_ptr
make_shared 提供了更简洁的构造语句。而且 make_shared 有机会将引用计数存储在其关联对象的相邻位置。
shared_ptr<X> p1 { new X{2} }; // BAD
auto p = make_shared<X>(2); // GOOD
使用 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 就已经有了
// 可行,但重复出现 Foo
unique_ptr<Foo> p {new Foo{7}};
// 更好的写法:避免重复的 Foo
auto q = make_unique<Foo>(7);
如果一个 unique_ptr 是由 new 的结果构造而不是 make_unique,则给出警告。
R.24:使用 std::weak_ptr 打破 shared_ptr 的循环引用
shared_ptr 依赖于引用计数,而循环结构的引用计数永远不为 0,因此我们需要一种机制来打破循环结构。
#include <memory>
class bar;
class foo {
public:
explicit foo(const std::shared_ptr<bar>& forward_reference)
: forward_reference_(forward_reference)
{ }
private:
std::shared_ptr<bar> forward_reference_;
};
class bar {
public:
explicit bar(const std::weak_ptr<foo>& back_reference)
: back_reference_(back_reference)
{ }
void do_something()
{
if (auto shared_back_reference = back_reference_.lock()) {
// 使用*shared_back_reference
}
}
private:
std::weak_ptr<foo> back_reference_;
}
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
// Boost 的 intrusive_ptr
#include <boost/intrusive_ptr.hpp>
void f(boost::intrusive_ptr<widget> p)
{
p->foo();
}
// Microsoft 的 CComPtr
#include <atlbase.h>
void f(CComPtr<widget> p)
{
p->foo();
}
p 是一个共享指针,但在这里没有用到它的共享性,并且通过值传递会导致性能下降;函数只有在需要参与 widget 的生命周期管理的时候使用智能指针。否则,应该使用 widget& 或者 widget*(如果实参可能是 nullptr)作为形参。
这些第三方/自定义的智能指针与 std::shared_ptr 概念一致,因此接下来的规则也适用于其他类型的第三方和自定义智能指针。这对于排查常见的智能指针错误、性能问题时非常有用。
R.32: 将形参声明为 unique_ptr<widget>
来表明函数对 widget 的所有权
R.33: 将形参声明为 unique_ptr<widget>&
来表明函数对 widget 的重新赋值语义
以这种方式使用 unique_ptr 既可以起到 self-documented 的作用,又可以强制执行函数调用的所有权转移或重新赋值(reseat)语义。
注意:“重新赋值”(reseat)的意思是:使指针或智能指针指向不同的对象。
// 接受 widget 的所有权
void sink(unique_ptr<widget>);
// 仅使用 widget
void uses(widget*);
// 可能重新赋值指针
void reseat(unique_ptr<widget>&);
// 通常不是你想要的
void thinko(const unique_ptr<widget>&);
- 如果一个函数通过左值引用接受
unique_ptr<T>
参数,并且在某条代码路径上没有对其进行赋值或调用 reset(),则给出警告。建议改为接受T*
或T&
。 - 如果一个函数通过 const 引用接受
unique_ptr<T>
参数,则给出警告。建议改为接受 const T* 或 const T&。
R.34: 将形参声明为 shared_ptr<widget>
来明确表明函数共享所有权
R.35: 将形参声明为 shared_ptr<widget>&
来表明函数可能重新赋值共享指针
R.36: 将形参声明为 const shared_ptr<widget>&
来表明它可能增加对象的引用计数
注:“重新赋值”(reseat)的意思是:使引用或智能指针指向不同的对象。
class WidgetUser
{
public:
// WidgetUser 将共享 widget 的所有权
explicit WidgetUser(std::shared_ptr<widget> w) noexcept:
m_widget{std::move(w)} {}
// ...
private:
std::shared_ptr<widget> m_widget;
};
void ChangeWidget(std::shared_ptr<widget>& w)
{
// 改变调用者的 widget
w = std::make_shared<widget>(widget{});
}
// 共享所有权,增加引用计数
void share(shared_ptr<widget>);
// 可能重新赋值指针
void reseat(shared_ptr<widget>&);
// 可能增加引用计数
void may_share(const shared_ptr<widget>&);
- 如果函数通过左值引用接受
shared_ptr<T>
参数,并且在某条代码路径上没有对其进行赋值或调用 reset(),则给出警告。建议改为接受 T* 或 T&。 - 如果函数通过值传递或 const 引用接受
shared_ptr<T>
参数,并且在某条代码路径上没有将其复制或移动到另一个 shared_ptr,则给出警告。建议改为接受 T* 或 T&。 - 如果函数通过右值引用接受
shared_ptr<T>
参数,建议改为值传递。
R.37: 不要传递从“智能指针别名”取得的指针或引用
违反本规则是导致丢失引用计数、产生悬空指针的首要原因。函数应优先考虑传递裸指针或引用到调用链的下游。在调用树的顶部,从智能指针取得裸指针或引用时,需要确保智能指针在调用树内部不会无意中被重置或重新赋值。
注:有时需要对智能指针进行本地拷贝,以保证在函数调用树的整个期间保持对象不被释放。
// 全局(静态或堆),或者本地智能指针的别名
shared_ptr<widget> g_p = ...;
void f(widget& w)
{
g();
use(w); // A
}
void g()
{
// 如果这是最后一个指向 widget 的 shared_ptr,销毁 widget
g_p = ...;
}
下面的代码无法通过 code review
void my_code()
{
// BAD: 传递一个“从非本地智能指针获得的指针或引用”,可能在 f(或 f 调用的函数)中不小心被重置
f(*g_p);
// BAD: 原因同上,只是作为 "this" 指针传递
g_p->func();
}
解决办法:拷贝一份副本,确保函数调用树整个期间引用计数不为 0
void my_code()
{
// 引用计数增加了 1,可以覆盖整个函数执行及调用树
auto pin = g_p;
// GOOD: 传递一个从“本地非别名的指针指针”获得的指针或引用
f(*pin);
// GOOD: 同上
pin->func();
}
- 如果在函数调用过程中使用的指针或引用是从一个非本地的 shared_ptr 或 unique_ptr 取得,或者从一个本地、但可能是别名的智能指针取得,给出警告。
- 如果是 shared_ptr,建议保存一个本地副本,然后从该副本获取指针或引用。
Recommend
-
13
在了解了Rust中的所有权、所有权借用、生命周期这些概念后,相信各位坑友对Rust已经有了比较深刻的认识了,今天又是一个连环坑,我们一起来把智能指针刨出来,一探究竟。 智能指针是Rust中一种特殊的数据结构。它与普...
-
15
VsCode Go插件配置最佳实践指南xcrossed区块链攻城狮 互连网从业者,程序coding近期组内...
-
11
智能指针避坑指南——几种常见的错误用法 2020-11-29...
-
11
ASP.NET Core Web API 最佳实践指南前文传送门:01 介绍当我们编写一个项目的时候,我们的主要目标是使它能如期运行,并尽可能地满足所有用户需求。但是,你难道不认为创建一个能正常工作的项目还不够吗?同时这个项目...
-
8
零一间2021.07.05 10:56:13字数 3,733阅读 25Python最佳实践指南! 您好,地球人!欢迎来到Python最佳实践指南。 这是...
-
8
以太坊上的核心开发者 Austin以太坊上的核心开发者 Austin | 以太坊上的最佳开发实践 以太坊核心开发者...
-
10
在容器环境中,Kubernetes管理着拥有数个、数百个甚至数千个节点的容器集群,其配置的重要性不可忽略。Kubernetes的配置选项很复杂,一些安全功能并非默认开启,这加大了安全管理难度。如何有效地使用包括Pod安全策略、网络策略、API服务器、Kubelet及其他Ku...
-
4
作者:lucasfan,腾讯 IEG 客户端开发工程师 智能指针在 C++11 标准中被引入真正标准库(C++98 中引入的 auto_ptr 存在较多问题),但目前很多 C++开发者仍习惯用原生指针,视智能指针为洪水猛兽。但很多实际场景下,智能指针却是解决问题的...
-
9
1 smart pointer 思想 个人认为smart pointer实际上就是一个对原始指针类型的一个封装类,并对外提供了-> 和 * 两种操作,使得其能够表现出原始指针的操作行为。 要理解smart...
-
5
最近在整理一些 C++ 智能指针的使用和避坑方面的资料,感兴趣的不妨看看 std::unique_ptr unique_ptr 它是一种独占资源所有权的指针,unique_ptr 会在栈上分配,然后在离开作用域之后进行释放,删除里面持有的 Resource...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK