-
很多编程语言中垃圾回收(GC)的确是非常便利的特性,但它们执行的时间往往不可预期;而C++98的全手动资源管理又显得过于“原始”了一点。将能够自动进行操作(GC)以及操作的时机可预期(析构函数)这两个优势结合在一起的,就是C++11的
std::shared_ptr
。 -
任一个
std::shared_ptr
都不拥有它管理的对象;但它们共同确保对象不再被需要时会被销毁。其中主要的实现机制是 引用计数(reference count):一个资源被多少个std::shared_ptr
指向的计数。std::shared_ptr
的大部分构造函数会使该值增加1(移动构造除外,左右抵消不需要增减,因此也比复制更快),析构函数会使该值减小1,复制赋值运算符同时增减(左减右加)。如果std::shared_ptr
发现经自减后引用计数变为0,则销毁该对象。 -
性能上来说:
std::shared_ptr
的大小是两倍裸指针,一个指针指向资源,一个指向引用计数(严格说是包含引用计数的 control block,见下文)。- 引用计数占用的内存必须是动态分配的,因为被指向的对象本身对引用计数毫不知情,因此必须由
std::shared_ptr
对引用计数进行动态分配和管理。(笔者注:C++的对象模型不像Java等有一个公共基类Object,因此如果采取将实现引用计数的责任甩给用户类编写者的设计,则会大大限制std::shared_ptr
的使用场景,也无法支持 built-in types。) - 引用计数的自增和自减必须是原子操作,否则线程不安全,而原子操作相对较慢。
-
笔者注:到这里你可能会质疑循环引用的问题——的确,当出现循环引用时引用计数永远不会归零,造成内存泄漏。C++的解决办法是引入
std::weak_ptr
,见下一节 Item 20 的讲解。 -
std::shared_ptr
默认也使用delete
作为资源释放方式,支持自定义 deleter。但与std::unique_ptr
设计不同的是,std::shared_ptr
的 deleter 不属于其类型的一部分,这样的设计使其更加灵活,例如可以将多个持有对象种类相同而 deleter 不同的对象互相赋值,或放在同一个容器中。(注:而std::unique_ptr
的根本设计理念是 lightweight,zero-overhead,使用默认deleter还多占一个指针的空间是不可接受的。)
std::unique_ptr<Widget, decltype(customDeleter1)> upw(new Widget, customDeleter1);
std::shared_ptr<Widget> pw1(new Widget, customDeleter1);
std::shared_ptr<Widget> pw2(new Widget, customDeleter2);
std::vector<std::shared_ptr<Widget>> vpw{ pw1, pw2 };
pw1 = pw2;
-
std::shared_ptr
使用的 deleter 大小不会影响其自身的大小,因为实际上std::shared_ptr
是将引用计数,weak count(见 Item21),自定义 deleter 等内容打包在了一个名为 控制块(control block) 的数据结构中,其自身保存两个指针:一个指向管理对象,一个指向控制块。
-
控制块理应由管理某对象的第一个
std::shared_ptr
创建;后续的std::shared_ptr
只需修改其中的数据。然而std::shared_ptr
无法知道自己是否是第一个指向某对象的,因此有必要应用以下规则:std::make_shared
(见 Item 21)总会创建一个控制块。它生成一个新对象,肯定是该对象的第一个所有者,所以应该生成控制块。- 当
std::shared_ptr
从一个独占性指针中构建时(即std::unique_ptr
或std::auto_ptr
)创建控制块。因为这些独占性指针不使用控制块。 - 当
std::shared_ptr
从一个裸指针中构建时,创建控制块;而从另一个std::shared_ptr
或std::weak_ptr
构建时不会创建控制块。
-
问题可能出现在第三条规则中:如果使用同一个裸指针创建了多个
std::shared_ptr
,就会对同一个对象创建多个控制块,最终导致对象被 delete 多次——undefined behavior!因此记住两点:(1)尽可能避免将裸指针传入std::shared_ptr
的构造函数;使用std::make_unique
。但这样无法使用自定义 deleter;(2)如果是这种情况,将new
的结果直接传入构造函数,不要用一个中间裸指针变量存储。 -
与此有关的一个坑点出现在
this
指针上。例如你要在一个类的函数中将当前对象的指针加到一个容器,如vector<std::shared_ptr<Widget>> v
中,可能自然会写出v.push_back(this)
的语句。如果在外面还有其他语句用当前对象创建过std::shared_ptr
,就会导致 undefined-bahavior。为此C++11提供了一个辅助性模板基类enable_shared_from_this<T>
。使用方法见以下代码。其中派生类作为基类模板类型的设计称为 奇异递归模板模式(Curiously Recurring Template Pattern,CRTP),我曾经在C#编程系列中单例模板类的设计中使用过:Unity之C#学习笔记(16):单例模式及单例模板类 Singleton and MonoSingleton
class Widget : public std::enable_shared_from_this<Widget> { // CRTP
public:
static std::shared_ptr<Widget> create() { // 仅允许使用工厂函数创建shared_ptr<Widget>对象,见下面解释
return std::shared_ptr<Widget>(new Widget);
}
using spwVector = std::vector<std::shared_ptr<Widget>>;
void addToVector(spwVector& v)
{
v.push_back(shared_from_this()); // 返回从this指针构建的shared_ptr
}
private:
Widget() = default; // 禁止直接创建Widget对象,见下面解释
};
shared_from_this()
不会创建控制块,而是在既有的控制块上创建std::shared_ptr
(查看源码发现其内部实际使用std::weak_ptr
存储派生类this
指针)。因此必须保证调用它之前至少用当前对象创建过一个std::shared_ptr
,否则C++17之后会抛出std::bad_weak_ptr
异常,C++17之前是 undefined behavior。 良好的方式是使该类对象仅能用返回std::shared_ptr<Widget>
的工厂函数创建,而通过private
阻止外部调用该类的构造函数。
- 小结——使用
std::shared_ptr
的得失:- 失:
std::shared_ptr
本体一般占用两个字长;控制块使用默认 deleter 和 allocator 时占用三个字长。引用计数涉及两个(自增、自减)原子操作,稍微有一些性能损失。 - 得:解引用效率基本与裸指针相同,动态分配资源生命周期的全自动管理。
- 失:
对大多数情况来说这是一笔很划算的买卖。当然,如果可能使用独占性拥有的设计,std::unique_ptr
是更佳的选择。它的性能更好,而且从 std::unique_ptr
“升级”到 std::shared_ptr
是很简单的(一个构造函数或等号赋值即可),反之则不成立。
- C++17之前,
std::shared_ptr
不能用于数组。虽然也可以用std::shared_ptr<T> a(new T[N])
将数组伪装成一个普通对象的指针,但这样的设计非常糟糕(默认 deleter 是错误的,应该提供用delete []
的 deleter;没有索引运算符operator[]
;std::shared_ptr
的派生类向基类转换的行为是错误的),完全没有使用的理由。C++17中用于数组的创建方式为shared_ptr<T[]> sp(new T[N]);
总结
std::shared_ptr
提供了一种便利的任意共享资源生命期管理(垃圾回收)方式。- 与
std::unique_ptr
相比,std::shared_ptr
通常占用两倍大小,有使用控制块的额外代价,以及引用计数需要使用原子操作。 - 默认的资源释放方式是通过
delete
,但可以使用自定义 deleter。deleter 的类型不影响std::shared_ptr
的类型。 - 避免从裸指针变量创建
std::shared_ptr
。
附:对MSVC的STL中 std::shared_ptr
实现的一些分析
此部分网络上基本没有参考资料,主要基于笔者阅读源码的推测,可能有错误,欢迎各位大神指出。
shared_ptr
继承于_Ptr_base
类(也是weak_ptr
的基类),后者包含两个主要数据成员_Ptr
和_Rep
,分别是管理对象的指针和引用计数的控制块指针(不过MSVC和GCC都没有使用 control block 的名称)。remove_extent_t
处理数组类型,移除长度标识(_Ty
非数组类型时返回值仍为_Ty
)。_Ptr_base
定义了:_Copy_construct_from
:复制构造,将_Ptr
和_Rep
换为参数_Other
的,并将计数+1。_Move_construct_from
:移动构造,将_Ptr
和_Rep
换为参数_Right
的,并将_Right
的置为nullptr
,计数不变。_Incref
和_Decref
:增加和减少计数,真正的实现在_Rep
中。
- 转到核心的引用计数实现部分:定义了一个基类
_Ref_count_base
和三个派生类_Ref_count
,_Ref_count_resource
和_Ref_count_resource_alloc
,_Ty
和_Resource
是管理对象类型(为什么要取两个名字?),_Dx
是 deleter 类型,_Alloc
是 allocator 类型,分别代表shared_ptr
的三种初始化方法。对象指针的销毁管理也是在这里而非_Ptr_base
或shared_ptr
中直接进行的。
- 基类
_Ref_count_base
定义了函数:_Destroy
和_Delete_this
:销毁管理对象的指针和自身,纯虚函数,在三个派生类中各自实现。_Incref
和_Decref
等:引用计数增减的真正实现,具体代码太菜了看不懂(逃)。总之如果_Uses
计数减为 0 则调用_Destroy
,如果_Weaks
计数减为 0 则调用_Delete_this
。
- 三个派生类主要内含数据成员以及上面提到的
_Destroy
和_Delete_this
实现:_Ref_count
类:仅存一个管理对象的指针_Ty
,_Destroy
行为是delete
。_Ref_count_resource
类:与unique_ptr
类似的实现手法,使用_Compressed_pair<_Dx, _Resource>
存储 deleter 和管理对象,_Destroy
行为是从_Compressed_pair
中取出 deleter 并在对象上调用。_Ref_count_resource_alloc
类:套娃的_Compressed_pair
,_Destroy
行为同上,_Alloc
的构造行为在shared_ptr
中(见下文),析构行为调用_Deallocate_plain
(Allocator还没学,不懂,逃x2)
-
回到
shared_ptr
中,先看构造函数。使用裸指针,deleter 和 allocator 的构造函数如下:
-
仅裸指针版本对于单个对象创建
_Temporary_owner
,然后调用_Set_ptr_rep_and_enable_shared
;对于数组调用_Setpd
。加入 deleter 和 allocator 则分别调用_Setpd
和_Setpda
。_Setpd
和_Setpda
内部实际上也先创建_Temporary_owner_del
,然后调用_Set_ptr_rep_and_enable_shared
。
-
_Temporary_owner
和_Temporary_owner_del
只是两个简单封装的结构体,笔者暂时没理解为什么在构造shared_ptr
时要用这么一层结构。 -
_Set_ptr_rep_and_enable_shared
实际设置好继承自基类的_Ptr
和_Rep
。
-
shared_ptr
的复制/移动构造函数,调用前面讲到的基类的_Copy_construct_from
和_Move_construct_from
实现。
-
shared_ptr
从std::unique_ptr
的构造函数:将对方的指针和 deleter 拿过来,类似裸指针+deleter的构造方式,然后将对方release
掉。
-
shared_ptr
的复制/移动赋值运算符:copy-and-swap idiom,用_Right
构造新的shared_ptr
,然后与*this
交换。swap
的实现就是分别对两个数据成员_Ptr
和_Rep
调用std::swap
。
-
reset
函数,同样是 copy-and-swap idiom,对应三种裸指针,deleter,allocator 的版本。
-
析构函数,调用基类的
_Decref
函数足矣,因为shared_ptr
自身中没有除基类_Ptr_base
外的数据成员(因此其大小也与_Ptr_base
相等,为两个指针)。对象销毁操作在_Ptr_base
→_Rep
的_Decref
中完成。