智能指针包括shared_ptr
,weak_ptr
和unique_ptr
,
如果接口中作为参数去传递,那么需要考虑是pass by value
还是by ref
,
如果传入了,又该如何使用才能尽量鲁棒,
可有够纠结的
WHY NOT
首先要考虑的是为什么不用raw的指针[1],那当然是有问题
除了常规的忘记delete、多次delete问题以外
其实更应考虑的是两个麻烦的问题
一是指针无法感知指向对象是否仍然存活
二是指针无法彻底禁用delete
,无论你怎么const
(就是说,你永远不知道一个指针抛出去给接口后会怎样)
HOW
智能指针用到了RAII,所以问题零是可以轻易解决的
又因为存在引用计数,问题一的解答就是use_count为0的时候即消亡
问题二就显得有点微妙,因为传参可以传引用(不会真的有人传智能指针的指针吧,不会吧不会吧)
因此还是可以随时干掉又暴露出去的
如果类设计只涉及到问题零的层级,那自行封装一层指针的RAII即可(比如完全不对外暴露/通信)
但是当指针需要逸出到外部,就需要考虑问题二,涉及到多线程,更需要考虑问题一
CHOOSE
考虑最简单的unique_ptr,它是不允许拷贝的,
传入要么是&
,表示可读写(套上const
就是不给你写),
要么是by val
+move
,表示转移控制权
由于独占的特性,不需要所谓的引用计数,
但是移动而非拷贝的做法凉的更快
比如
if(ptr) {
// ... 被其他线程移动
ptr->func();
}
而对于shared_ptr
由于具有拷贝的特性支持,完全可以
if(auto localPtr = globalPtr) { // 我持有一个,
localPtr->func();
} else // 或者本来global就没有
因此即使被move,也依然有一定的余地可感知是否持有资源(如果是by value
的形式,那连第一步都省了)
同理,weak_ptr
作为shared_ptr
的配套工具,也是(且必须)这么使用的(lock()
)
作为共识,我们不考虑它们强弱引用计数之间所谓的性能差异,只关注所有权,
比如观察者模式中Subject
持有Observer
时是否要延长其生命周期
是则前者,否则后者
至于by ref
的形式,我觉得是没必要了,因为为了安全还是要搞一个localPtr
,显得多此一举
(Update.想了一下,或许是传递多层的参数(而不读写)还是值得一用?)
SUMMARY
- 一旦转移所有权,智能指针的线程安全问题需要额外保障
- 个人观点而言,
unique_ptr
的使用一般就用于浅层的RAII封装(比如纯粹用在可控的类内部,或者std::vector<指针>
这种更加方便(自定义)析构需求) weak_ptr
和shared_ptr
的传参抉择在于生命周期该怎么设计,前者更强调读写这一操作,写到接口上很容易明白要干什么
PS.循环引用、构造函数和this
指针的坑都不在本话题范围内,但是用到了基本都要面对这些问题
[1] 如果考虑该接口为算法层面,确实可以用普通指针,但还是应该以sp->get
的形式把逸出的粒度降到最小,且保证不影响生命周期和不再传出;在这里需要考虑的不只是性能问题,虽然性能实测过和unique_ptr
几乎一致,更考虑的是迭代偏移操作的表达,智能指针并没有提供这种特性,而且智能指针与STL/algortihm
提供的接口更是完全搭不上,到最终还是不可避免地用回普通指针
随想部分
其实这种设计上的抠细节我觉得没啥意思,除了像多线程的坑有点难处理以外,其他的情况都可以通过自己代码上的规范来避免,
毕竟,真要钻空子,sp->get
也可以拿到一个危险的指针(当然也可以弄个更严格的类继承智能指针把它干掉)
而有些场景,像是用到了算法的高性能需求,或者我本身就明确知道它的内存布局,需要++/--
的层面,用智能指针这种处理所有权的工具就明显不适合
或者你在一个继承链/组合关系上能了解到这个接口肯定能在生命周期内完成,那为啥还要多加入一层不必要的条条框框的封装
或者说,用一套自行规约的机制,智能指针掌握所有生命周期,raw指针仅具有使用权(observer_ptr
/unique_ptr&
),那整体接口的设计用途从传参类型就能知道它大概的用途和副作用