1
  • unique_ptr是拥有独立对象所有权语义的智能指针,换言之,一个 unique_ptr对象所拥有的指针只允许它自己占有,不允许多个对象共享(这里希望大家理解语义和语法规则的区别,从语义上来说unique_ptr的指针不允许共享,但c++的语法规则并不禁止这么做,当然后果就是未定义的了。)
  • unique_ptr是一个模板类,其拥有两个模板参数,第一个参数是该对象持有指针指向的类型,第二个参数是删除器的类型。
  • unique_ptr有两个版本,第一个版本是默认的管理单个对象的版本,第二个版本是通过偏特化实现的管理动态分配的数组的版本。在cppreference网站上这个模板类的声明是这个样子:

1.png

  • 在vs2017中它是这个样子:

2.png

  • unique_ptr可以被移动构造和移动赋值,但不能被复制构造和复制赋值。(这就是拥有独立对象所有权这一理念的实现形式之一)

3.png

  • 可以看到,在vs2017中unique_ptr模板类的复制构造函数和复制赋值函数已经被声明为delete。
  • unique_ptr对第二个模板参数,也就是删除器类型具有如下要求:Deleter必须是函数对象(FunctionObject)或者函数对象的左值引用,或者是函数(function)的左值引用,其应该可以通过一个类型为unique_ptr<T,Deleter>::pointer的参数被调用。这里解释一下什么是FunctionObject,c++中的FunctionObject类型的对象可以被使用在call operator(就是()这个调用运算符)左边。
  • 注意:只有非常量的unique_ptr能够将其管理对象的所有权转移到另外一个unique_ptr对象中。如果一个对象的生命周期由一个const std::unique_ptr所管理,它就会被限制到该指针创建的scope中。
  • 上面这段话说了什么意思呢?我们看下面这样的代码:

4.png

  • 首先定义并初始化一个unique_ptr类型的对象,然后将其管理的指针转移到另外一个对象new_ptr中,最后我们打印出new_ptr管理的int*对象指向的值。

5.png

  • 执行成功。那么如果我们将ptr的类型加上常量属性呢?

6.png

  • 会产生如下的一个编译错误:

7.png

  • 这个错误是说,尝试去调用unique_ptr的复制构造函数,而这个函数如上所说已经被我们删除。在c++中,具有const属性的rvalue expression并不能被右值引用所捕获,其只能被常量左值引用所捕获。在第二个版本中,std::move(ptr)返回的值是一个具有const属性的xvalue的值,其只能被复制构造函数所捕获。
  • unique_ptr可以被一个不完整类型T构造(其第一个模板参数T可以是不完整类型),如果使用默认的删除器的话,在删除器被调用的点处T类型必须要是完整的,这些点包括析构函数、移动赋值函数、reset成员函数(而对应的shared_ptr不能够被一个不完整类型的指针构造,但是可以在T为不完整类型处释放)。
  • 如果T是某个基类B的派生类,那么std::unique_ptr<T>将会被隐式的转化为std::unique_ptr<B>,而std::unique_ptr<B>的默认删除器将会按照B类型来释放指针,如果B的析构函数不是virtual的话将会导致未定义行为。注意std::shared_ptr表现不同,即使基类的析构函数不是virtual的,其也可以调用正确的析构函数。
  • 该类具有的成员类型、函数和变量不在赘述,标准中写的很清楚,见:https://en.cppreference.com/w...
  • 下面我们看一下vs2017中,unique_ptr的一些实现细节:

10.png

11.png

  • 两个版本的unique_ptr都继承自一个共同的基类_Unique_ptr_base,这也是一个模板类,第一个模板参数是持有指针对应的类型(这里已经去除了单个对象和数组对象的区别,那么如何在析构函数中调用正确的delete呢?往后看),第二个模板参数是删除器的类型。

12.png

  • 标准中要求的pointer成员类型也是由该基类导出。

13.png

在声明中可以看到,默认的删除器类型是default_delete<_Ty>类型,其中default_delete也是一个模板类,该模板参数就是要删除指针指向的类型。

14.png

  • _Unique_ptr_base模板类中,首先通过类型萃取得到去除引用的删除器类型:_Dx_noref。可以看到,pointer类型由_Get_deleter_pointer_type这个模板类导出。

15.png

  • 这个类通过特化实现了这样一个逻辑:如果_Dx_noref类型中有pointer的成员类型,则将其作为type类型导出,否则将_Ty* 作为type类型导出。导出的就是unique_ptr中的pointer类型了。也就是说,我们可以通过对删除器中添加一个pointer的成员类型来定制化unique_ptr。
  • 对于pointer的类型,标准中有如下说明:

16.png

  • 如果std::remove_reference<Deleter>::type::pointer存在的话,就是这个类型,否则就是T*类型,这与源代码是相一致的。但是还有一个限制:必须要符合NullablePointer。我们在标准中继续查看NullablePointer的含义。NullablePointer类型是指 该类型的对象能够与std::nullptr_t类型对象进行比较的类似于指针的对象。(建议看英文原文,翻译过来很别扭)
  • 好了我们接着看_Unique_ptr_base模板类,这个模板类中有这样一个成员变量:

18.png

  • 这里又引入一个新的模板类:_Compressed_pair。这个模板类其实就存储了两个对象,第一个是删除器类型的对象,第二个是我们存储的指针类型的对象。既然如此简单我们为什么要专门再用一个模板类来做一层抽象呢?这里其实对当删除器类型是一个空类的情况做了一个优化。例如默认的删除器,其实我们只需要调用它的某个成员函数即可,完全不必要存储任何成员变量,但是c++中的空类(即没有数据成员的类)会占据一个内存字节(这是为了让对象的实例能相互识别,为了让每个实例在内存中都有独一无二的地址),当这个空类作为成员函数在另外一个类中存在时,由于内存对齐的原因会消耗更多的内存地址。看下面这个例子:

19.png

  • A是一个空类,其作为B类中第一个数据成员,B中第二个数据成员为int类型变量。由于int类型变量占据4个字节,由于内存对齐的原因,a1和a2之间还有3个字节的空白。Sizeof(B)返回的结果是8个字节。
  • _Compressed_pair模板类就是针对这个现象做了一个优化,当删除器类型是空类时,通过继承的方式获得其成员函数和成员类型等信息而不用额外的内存消耗。

20.png

  • _Compressed_pair模板类有三个模板参数,第一个是删除器类型,第二个是存储的指针对应的类型,第三个是为了做特化的类型bool。如果删除器类型是空类型并且是一个final类型(不能被继承),就使用默认的版本,即私有继承删除器类型,仅仅有一个_Ty2类型的成员变量。当第三个模板参数求值为false时,采用其特化版本:

21.png

  • 可以看到,当删除器类型不是空类型时,则按照常规处理方法保持两个成员变量即可。
  • 这里花了一点篇幅主要介绍了unique_ptr内存优化的部分,接下来我们看下默认删除器是如何进行删除操作的。

22.png

  • default_delete同样根据_Ty类型是单一类型还是数组类型做了特化处理。

23.png

24.png

  • default_delete有一个构造函数,该函数是一个模板函数,如果_Ty2 是_Ty1 的子类,则可以用_Ty2类型的删除器删除_Ty1类型的指针。
  • 对()运算符的重载则是删除时真正调用的函数了,可以看到在单一类型的版本中调用了delete,而在数组类型的版本中调用了delete[]。

p__n
491 声望10 粉丝

科学告诉你什么是不可能的;工程则告诉你,付出一些代价,可以把它变成可行,这就是科学和工程不同的魅力。