C/C++学习记录:智能指针 std::unique_ptr 源码分析
Posted 河边小咸鱼
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C/C++学习记录:智能指针 std::unique_ptr 源码分析相关的知识,希望对你有一定的参考价值。
- 抽空扣一点感兴趣的标准库源码,这里总结一下
std::unique_ptr
相关的分析 - 本文中 gcc version: 8.4.1 20200928 (Red Hat 8.4.1-1) (GCC)
- libstdc++:
libstdc++-8.4.1-1.el8.x86_64
- 其中c++库安装路径为
/usr/include/c++/8
目录
一、前言
在对本篇文章内容的探索之前,我已经是对 std::unique_ptr
有所了解并且尝试实现过,所以本篇分析主要是为了查看标准库里的具体实现和写法,学习一下细节和思路。
众所周知啊 ,std::unique_ptr
的主要思路就是利用局部变量声明在栈上,会自动释放的特性,在构造函数里绑定指针,在析构函数里释放此指针的内容,从而达到智能指针的效果(自动释放)。这种机制被称为RAII机制,相关类被称为RAII类。
本篇笔记中会记录我在学习标准库代码时的思路和想法,此次我看的重点是自动释放的流程和相关数据结构的实现。
二、源码分析
1. 从 class unique_ptr 入手
class unique_ptr
的定义位于/usr/include/c++/8/bits/unique_ptr.h
中。
首先可以看到,在这个文件中,存在两个 unique_ptr
类的定义,如下两图:
- 定义一:
- 定义二:
根据注释以及部分来看代码,一百多行开始的定义一部分是针对 single objects
的,而四百多行开始的定义二部分是针对 array objects
的。简单来讲就是定义一是针对单个指针对象的,而特化版本定义二是针对存有多个指针的数组对象的。
另外,可以看到模板中有两个参数,_Tp
和 _Dp
。根据之前对 std::unique_ptr
的使用来看,第一个参数是指针所指向的类型;然后我简单看了一下源码和注释,第二个参数是删除器,即规定指针释放时的操作。
在定义一中,删除器被默认指定为 default_delete<_Tp>
,在下文中会对其进行分析。而定义二中需要传入两个模板参数,指定类型和删除器,我认为原因是指针对象数组与单个指针对象析构时存在差异,所以需要单独定义删除的方法。
接着就是在智能指针中,肯定是存在一个成员变量来储存指针的,所以我决定先看一下这部分内容标准库里是怎么实现的。如下图,是 std::unique_ptr
的一个构造函数,其中传入 pointer
类型的 __p
并将其赋值到成员变量 _M_t
中。
首先查看 pointer
的定义,为 using pointer = typename __uniq_ptr_impl<_Tp, _Dp>::pointer;
。可以看到一个新的类型 __uniq_ptr_impl
出现了。了解了 pointer
的定义后再看一下成员变量 _M_t
,定义如下:
可以看到其类型也为 __uniq_ptr_impl
,且模板传参为指针类型与其删除器。由此可以大概了解流程,即 std::unique_ptr
中存在一个 __uniq_ptr_impl
类型的成员变量,其中储存着传入的指针以及其删除器。在传参指针传入后,会将其赋值至成员变量 _M_t
中,并根据传参内容来决定删除器的内容,也储存至 _M_t
中。而接下来,我对这个储存指针的类型 __uniq_ptr_impl
进行了分析和探索。
2. 储存类 class __uniq_ptr_impl
class __uniq_ptr_impl
的定义位于/usr/include/c++/8/bits/unique_ptr.h
中。
由上文可以得知,在 __uniq_ptr_impl
类中,储存着指针和其删除器。在下文中的分析中,我的重点在于储存这两个东西的实现,以及类中有哪些方法来完善其功能。下图为相关定义的位置:
首先在类 __uniq_ptr_impl
中的最前定义了两个结构体,简单来说其作用就是传入类型 a
,使用其内部的 type
即可析取出类型 a
的指针a*
。其中用到了 remove_reference
结构体,这个在之前的 std::move
源码分析中有讲解,它起到去引用的功能,而在这里使用它即可保证 type
能正确取到不带引用的指针类型。具体实现如下:
然后接下来是对构造函数的声明以及几个定义。首先是删除器的判定类型,可以实现对删除器的正确性的判定。接下来是一个 pointer
类型,就是对上文的 _Ptr
结构体中的 type
类型的使用,来取指针类型。然后是三个构造函数,一个是默认的空参构造,用了 C++11 中的default关键字;一个是仅传指针的构造函数,调用 _M_ptr
进行赋值;最后一个是传入指针和删除器的构造函数,赋值至成员变量 _M_t
中,这里用了 std::forward
来实现完美转发,也就是说这里的删除器可以是个右值。
然后是几个取成员变量内容的方法和一个swap函数。一共是4个取成员变量内容的方法,指针和删除器各两个。两个方法中上面一个是取引用,即可以改变内容的;下面一个是const修饰的,是不可改内容的,应该是只用于查看内容。下面那个swap函数里,即为使用 std::swap
交换 __uniq_ptr_impl
类中的内容,此函数作用于 std::unique_ptr
中的指针所有权转让的情景。
最后就是 __uniq_ptr_impl
类中的成员变量 _M_t
,其使用了 C++11 中新增的数据结构 std::tuple
来储存指针和其删除器。所以上文中取其内容是通过 std::get
来取的。
3. 默认删除器 struct default_delete
struct default_delete
的定义位于/usr/include/c++/8/bits/unique_ptr.h
中。
上文中看了储存类的定义,下面来看一下出现率也很高的删除器。标准库中提供了两个最基础的删除器,来当默认删除器。提供的两个删除器分别为普通对象删除器和特化版的数组对象删除器。当传入参数为一个数组时,就会走下面那个定义。
- 定义一:
- 定义二:
下面两个即为这两个删除器定义不同的最关键的地方。可以看到其中都存在 C++11 中的新特性静态断言,来进行合法性判定。而关键语句一个为delete __ptr
(85行),另一个为delete[] __ptr
(122行)。这即为默认删除器的核心语句,即单个对象通过delete释放,数组对象通过delete[]来释放。这里的实现是通过重载操作符()
来实现的,算是一种仿函数的写法。
4. 回归 class unique_ptr
通过上面对储存类和删除器的实现分析,已经大概了解了 std::unique
中的大部分封装好的内容。接下来就看一下在 class unique_ptr
中都有些什么东西。(此处仅记录重点内容)
首先是若干种构造函数:
- 空参构造
- 带参构造一,参数仅有指针
- 带参构造二,参数为指针和其删除器
- 带参构造三,传参为指针和删除器,但是都是右值
- 带参构造四,传参为一个空指针
- 带参构造五,传参为一个
std::unique
对象,这里涉及到引用折叠和完美转发,来实现所有权转移的操作
- 带参构造六,传参为一个
std::unique
对象,相比上文那个更加严谨
接下来是析构函数,如下图只有一种实现(单对象和数组对象都是这个)。可以看到逻辑很简单就是在析构的时候调用仿函数删除器来进行delete
操作。这里的 pointer
是获取空指针用的。
剩下的内容中,大部分都是对操作符 =
、*
、->
等的重载来方便设定智能指针的内容,以及方便使用其内容。另外还封装了几个函数如下,来获取成员变量 _M_t
中的指针和删除器,上面提到的若干重载也是基于下面这几个函数来实现的。另外还有一些基础的函数,例如清空智能指针用的 release()
或者 reset()
,还有交换内容的 swap
函数,这几个就不多说了,因为实现都比较简单。
在定义的最后还有两个被禁止的操作,即对 std::unique_ptr
进行左值赋值操作,从而来保证逻辑的正确执行。
三、总结
总而言之,std::unique
的主要思路还是比较简单的。但是在标准库源码中会有很多很细节的操作,所以还是有很大的收获。在源码的最后还有若干对 hash
相关的重载操作,在此文中就不进行记录了。
标准库的代码还是像套娃一样有很多层,前面看着会跳来跳去,但是了解底层以后,上面也就很清晰了。在接下来我应该会去看一下 std::shared_ptr
的实现,相对 std::unique_ptr
其肯定会复杂很多,但是这次搞完以后应该看起来也会轻松一点吧,毕竟思路还是有些相同点的,我的重点应该会放在其内部计数器的实现。
以上是关于C/C++学习记录:智能指针 std::unique_ptr 源码分析的主要内容,如果未能解决你的问题,请参考以下文章