C++智能指针和接口设计

智能指针包括shared_ptrweak_ptrunique_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

  1. 一旦转移所有权,智能指针的线程安全问题需要额外保障
  2. 个人观点而言,unique_ptr的使用一般就用于浅层的RAII封装(比如纯粹用在可控的类内部,或者std::vector<指针>这种更加方便(自定义)析构需求)
  3. weak_ptrshared_ptr的传参抉择在于生命周期该怎么设计,前者更强调读写这一操作,写到接口上很容易明白要干什么

PS.循环引用、构造函数和this指针的坑都不在本话题范围内,但是用到了基本都要面对这些问题

[1] 如果考虑该接口为算法层面,确实可以用普通指针,但还是应该以sp->get的形式把逸出的粒度降到最小,且保证不影响生命周期和不再传出;在这里需要考虑的不只是性能问题,虽然性能实测过和unique_ptr几乎一致,更考虑的是迭代偏移操作的表达,智能指针并没有提供这种特性,而且智能指针与STL/algortihm提供的接口更是完全搭不上,到最终还是不可避免地用回普通指针


随想部分

其实这种设计上的抠细节我觉得没啥意思,除了像多线程的坑有点难处理以外,其他的情况都可以通过自己代码上的规范来避免,

毕竟,真要钻空子,sp->get也可以拿到一个危险的指针(当然也可以弄个更严格的类继承智能指针把它干掉)

而有些场景,像是用到了算法的高性能需求,或者我本身就明确知道它的内存布局,需要++/--的层面,用智能指针这种处理所有权的工具就明显不适合

或者你在一个继承链/组合关系上能了解到这个接口肯定能在生命周期内完成,那为啥还要多加入一层不必要的条条框框的封装

或者说,用一套自行规约的机制,智能指针掌握所有生命周期,raw指针仅具有使用权(observer_ptr/unique_ptr&),那整体接口的设计用途从传参类型就能知道它大概的用途和副作用

发表评论

邮箱地址不会被公开。 必填项已用*标注