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>
来表明函数对 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,建议保存一个本地副本,然后从该副本获取指针或引用。
C++ 核心指南之资源管理(下)—— 智能指针最佳实践的更多相关文章
- 必须要注意的 C++ 动态内存资源管理(五)——智能指针陷阱
必须要注意的 C++ 动态内存资源管理(五)——智能指针陷阱 十三.小心使用智能指针. 在前面几节已经很详细了介绍了智能指针适用方式.看起来,似乎智能指针很强大,能够很方便很安全的管理 ...
- 【读书笔记】读《高性能网站建设指南》及《高性能网站建设进阶指南:Web开发者性能优化最佳实践》
这两本书就一块儿搞了,大多数已经理解,简单做个标记.主要对自己不太了解的地方,做一些记录. 一.读<高性能网站建设指南> 0> 黄金性能法则:只有10%~20%的最终用户响应时间 ...
- 智能合约最佳实践 之 Solidity 编码规范
每一门语言都有其相应的编码规范, Solidity 也一样, 下面官方推荐的规范及我的总结,供大家参考,希望可以帮助大家写出更好规范的智能合约. 命名规范 避免使用 小写的l,大写的I,大写的O 应该 ...
- 自己总结的C#编码规范--3.特定场景下的命名最佳实践
特定场景下的命名最佳实践 命名空间 要使用PascalCasing,并用点号来分隔名字空间中的各个部分. 如Microsof.Office.PowerPoint 要用公司名作为命名空间的前缀,这样就可 ...
- C++ 拷贝控制和资源管理,智能指针的简单实现
C++ 关于拷贝控制和资源管理部分的笔记,并且介绍了部分C++ 智能指针的概念,然后实现了一个基于引用计数的智能指针.关于C++智能指针部分,后面会有专门的研究. 通常,管理类外资源的类必须定义拷贝控 ...
- 智能指针思想实践(std::unique_ptr, std::shared_ptr)
1 smart pointer 思想 个人认为smart pointer实际上就是一个对原始指针类型的一个封装类,并对外提供了-> 和 * 两种操作,使得其能够表现出原始指针的操作行为. ...
- 读生产环境下go语言最佳实践有感
最近看了一篇关于go产品开发最佳实践的文章,go-in-procution.作者总结了他们在用go开发过程中的很多实际经验,我们很多其实也用到了,鉴于此,这里就简单的写写读后感,后续我也争取能将这篇文 ...
- linux下redis的最佳实践(Master-Slave)
本文演示了redis在同一台linux上的安装及运行多个实例,并演示了主从复制,以及如何进行主从的切换. 1. 下载 $ wget http://download.redis.io/releases/ ...
- shell下变量比较最佳实践
https://stackoverflow.com/questions/13617843/unary-operator-expected If you know you're always going ...
- 异常处理与MiniDump详解(2) 智能指针与C++异常
write by 九天雁翎(JTianLing) -- blog.csdn.net/vagrxie 讨论新闻组及文件 一. 综述 <异常处理与MiniDump详解(1) C++异常>稍 ...
随机推荐
- .NET CORE 部署到IIS上,HTTP 错误 500.19 - Internal Server Error
经排查,是因为项目中web.config的rewrite节点不支持,注释掉此节点即可,或者尝试下载相关依赖以支持此节点
- 指针和引用(pointer and reference),传值和传址
pass by adress pass by reference和pass by pointer的共同点都在于传址,都是对于对象的地址的复制,而不会对对象进行产生副本的操作. pass by refe ...
- [ [Ynoi2013] 无力回天 NOI2017 ] 解题报告
[Ynoi2013] 无力回天 NOI2017 首先看到异或,想到能维护异或的东西就那几样(线性基/01trie/数位 dp/FWT),再看到求选任意个数后的异或最大值,线性基无疑了. 这时再看还要维 ...
- golang中一种不常见的switch语句写法
最近翻开源代码的时候看到了一种很有意思的switch用法,分享一下. 注意这里讨论的不是typed switch,也就是case语句后面是类型的那种. 直接看代码: func (s *systemd) ...
- Java中数字相关的类有哪些?Nuber数字类和Math数学类详解
前言 我们在解决实际问题时,会经常对数字.日期和系统设置进行处理,比如在我们的代码中,经常会遇到一些数字&数学问题.随机数问题.日期问题和系统设置问题等. 为了解决这些问题,Java给我们提供 ...
- 基于SqlSugar的开发框架循序渐进介绍(29)-- 快速构建系统参数管理界面-Vue3+ElementPlus
在随笔<基于SqlSugar的开发框架循序渐进介绍(28)-- 快速构建系统参数管理界面>中介绍了基于SqlSugar开发框架,构建系统参数管理的后端API部分,以及WInform界面部分 ...
- 【Redis】数据类型介绍
一.字符 string Redis常用基本类型之一,存入Redis的所有key都是字符类型,常用于保存Session信息 字符类型 命令 含义 复杂度 set <key> <valu ...
- 字符串处理------Brute Force与KMP
一,字符串的简单介绍 例:POJ1488 http://poj.org/problem?id=1488 题意:替换文本中的双引号: #include <iostream> #includ ...
- 2022-10-17:特殊的二进制序列是具有以下两个性质的二进制序列: 0 的数量与 1 的数量相等。 二进制序列的每一个前缀码中 1 的数量要大于等于 0 的数量。 给定一个特殊的二进制序列 S,以
2022-10-17:特殊的二进制序列是具有以下两个性质的二进制序列: 0 的数量与 1 的数量相等. 二进制序列的每一个前缀码中 1 的数量要大于等于 0 的数量. 给定一个特殊的二进制序列 S,以 ...
- Django4全栈进阶之路14 项目实战(用户管理):base.html基础模板设计
在 Django 中,我们可以使用模板继承来避免代码的重复.模板继承是指我们可以在一个模板中定义一些公共的 HTML 代码,然后在其他模板中继承这个基础模板,并根据需要添加或覆盖一些内容. 通常情况下 ...