shared_ptr 和 unique_ptr 深入探秘
- C++ 中
shared_ptr和unique_ptr是 C++11 之后被广泛使用的两个智能指针,但是其实他们在使用上还是有一些“秘密”的,我根据平时遇到的两个问题,总结记录一些知识。
为什么 unique_ptr 需要明确知道类型的析构函数
这个问题是我写unique_ptr调试接口的时候才注意到的,之前确实不知道。为什么会这样呢?首先我们必须要知道unique_ptr到底封装了什么?通常unique_ptr就是简单的对裸指针封装,并且禁用拷贝和赋值:
template<classT,classDeleter=std::default_delete<T>>classunique_ptr;可以看到,Deleter的类型是unique_ptr类型的一部分。在unique_ptr内部会保存类型为T*和Deleter的成员 ,分别表示保存的裸指针和删除器。假设内部是这么实现的 (一般会运用空基类优化把Deleter的空间优化掉,libstdc++里把他们放进了一个 tuple。这里是简化了):
private:T*p;Deleter del;然后析构的时候就会这样:
~unique_ptr(){del(p);}当Deleter是默认的std::default_delete时,del(p)就会delete p,delete会调用析构函数。而delete一个不完整类型的指针是 ub(undefined behavior)。在典型的实现中都会在delete前通过static_assert(sizeof(T) > 0)做检查。sizeof对 incomplete type 求值会直接编译出错。
incomplete type 是指当定义一个变量的时候,不知道应该分配多少内存。C++ 声明和定义最大的区别就是是否发生内存分配,当发生内存分配的时候,必须知道要分配多少内存,通常一个未定义的 struct,未指定长度的数组类型,都会引发 incomplete type 的问题。参考:https://docs.microsoft.com/en-us/cpp/c-language/incomplete-types?view=msvc-170
structstudent;std::cout<<sizeof(student)<<std::endl;上述代码执行会报错
prog.cc:17:18:error:invalid application of'sizeof'to an incomplete type'student'std::cout<<sizeof(student)<<std::endl;只声明了结构体student,但是并没有定义,所以是一个 incomplete type,所以sizeof无法执行。
回到unique_ptr,现在我们知道unique_ptr的报错链路是unique_ptr->delete->sizoef,也就是sizeof才是罪魁祸首。所以当Deleter非默认时,就不一定需要知道类型的析构函数。比如下面这样:
// A is incomplete typeclassA;autoDel=[](A*){};std::unique_ptr<A,decltype(Del)>ptr;因此可以对这个问题做定性:并不是unique_ptr需要知道析构函数,而是unique_ptr的默认删除器Deleter需要明确知道类型的析构函数。
继续深挖一下,这个问题会出现在shared_ptr吗?答案是不会。这又引入了另一个问题,shared_ptr 和 unique_ptr 的封装有什么不同?
下面这段代码将演示
classTC;// 不提供析构函数voidfunc(){// unique_ptr<TC> up; // error: invalid application of 'sizeof' to incomplete type 'detail4::TC'shared_ptr<TC>tc;// shared_ptr可以正常运行}intmain(){func();}shared_ptr 的封装
按理说shared_ptr.reset的时候需要deleteptr 就需要 ptr 的类型(错了请指正),而shared_ptr的 template type 可以是 incomplete type(错误请指正)。cppreference 是这么描述的:
std::shared_ptrmay be used with an incomplete typeT. However, the constructor from a raw pointer (template shared_ptr(Y*)) and the templatevoid reset(Y*) member function may only be called with a pointer to a complete type (note that std::unique_ptr may be constructed from a raw pointer to an incomplete type).
reset的时候需要类型完整。默认构造的时候允许是不完整类型。为什么会这样呢?shared_ptr怎么处理Deleter呢?(还记得吧, Deleter 就是智能指针析构时候的删除操作)在常见编译器的实现里,
shared_ptr把Deleter(包括默认情况下的 operator delete)放进一个叫做control block的结构里,相当于做了一次 type erasure,把Deleter的类型从shared_ptr类型本身里面擦下去。Deleter的类型在 control block 的具体类型上,shared_ptr本身只持有一个control block基类的指针,通过虚函数来调用Deleter。而因为shared_ptr构造的时候要求必须是 complete type,control block已经知道怎么析构了,shared_ptr析构的时候就调用个虚函数,具体事情它不管的。这下我们明白了,
unique_ptr的封装太简单了,没有 control block,Deleter(包括默认的std::default_delete)直接做在unique_ptr一起了,这就导致unique_ptr的析构函数需要亲手析构被管理的类型,因此析构函数必须看到 complete type。然而反过来,因为构建的时候只需要保存下指针,所以unique_ptr构造的时候不需要看到 complete type。这俩正好是反的。C++ 标准并没有规定这些实现细节,但是规定函数签名和特性的时候,是考虑着比较合理的实现方式来写标准的,到最后标准落下来之后也差不多只能这么实现了。
总结
unique_ptr只保存了类型指针 ptr 和这个指针的析构方法,调用 delete ptr,就需要ptr的完整类型,为了防止这个问题出现,直接通过 assert sizeof 排除掉了这种风险。unique_ptr相当于在编译时绑定了删除器。shared_ptr保存的是一个控制块的指针。控制块包含的就是一个引用计数和一个原来对象的裸指针。控制块中初始化的指针是nullptr,在运行时为其赋值,也可以通过reset修改。类似于虚函数,shared_ptr相当于在运行时绑定了删除器。
虽然只是一个小小的知识点,但是也帮助我深入理解了shared_ptr和unique_ptr在设计上的区别,对于不同使用场景下选择不同智能指针的体会也更加深刻。