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

目录

  • 一、前言
  • 二、源码分析
    • 1. 从 class unique_ptr 入手
    • 2. 储存类 class __uniq_ptr_impl
    • 3. 默认删除器 struct default_delete
    • 4. 回归 class unique_ptr
  • 三、总结

一、前言

  在对本篇文章内容的探索之前,我已经是对 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 类的定义,如下两图:

  • 定义一:
    C/C++学习记录:智能指针 std::unique_ptr 源码分析_第1张图片
  • 定义二:
    C/C++学习记录:智能指针 std::unique_ptr 源码分析_第2张图片

  根据注释以及部分来看代码,一百多行开始的定义一部分是针对 single objects 的,而四百多行开始的定义二部分是针对 array objects 的。简单来讲就是定义一是针对单个指针对象的,而特化版本定义二是针对存有多个指针的数组对象的。

  另外,可以看到模板中有两个参数,_Tp_Dp。根据之前对 std::unique_ptr的使用来看,第一个参数是指针所指向的类型;然后我简单看了一下源码和注释,第二个参数是删除器,即规定指针释放时的操作。

  在定义一中,删除器被默认指定为 default_delete<_Tp>,在下文中会对其进行分析。而定义二中需要传入两个模板参数,指定类型和删除器,我认为原因是指针对象数组与单个指针对象析构时存在差异,所以需要单独定义删除的方法。

  接着就是在智能指针中,肯定是存在一个成员变量来储存指针的,所以我决定先看一下这部分内容标准库里是怎么实现的。如下图,是 std::unique_ptr 的一个构造函数,其中传入 pointer 类型的 __p 并将其赋值到成员变量 _M_t 中。
C/C++学习记录:智能指针 std::unique_ptr 源码分析_第3张图片
  首先查看 pointer 的定义,为 using pointer = typename __uniq_ptr_impl<_Tp, _Dp>::pointer;。可以看到一个新的类型 __uniq_ptr_impl 出现了。了解了 pointer 的定义后再看一下成员变量 _M_t,定义如下:
C/C++学习记录:智能指针 std::unique_ptr 源码分析_第4张图片
  可以看到其类型也为 __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 类中,储存着指针和其删除器。在下文中的分析中,我的重点在于储存这两个东西的实现,以及类中有哪些方法来完善其功能。下图为相关定义的位置:
C/C++学习记录:智能指针 std::unique_ptr 源码分析_第5张图片
  首先在类 __uniq_ptr_impl 中的最前定义了两个结构体,简单来说其作用就是传入类型 a,使用其内部的 type 即可析取出类型 a 的指针a*。其中用到了 remove_reference 结构体,这个在之前的 std::move 源码分析中有讲解,它起到去引用的功能,而在这里使用它即可保证 type 能正确取到不带引用的指针类型。具体实现如下:
C/C++学习记录:智能指针 std::unique_ptr 源码分析_第6张图片
  然后接下来是对构造函数的声明以及几个定义。首先是删除器的判定类型,可以实现对删除器的正确性的判定。接下来是一个 pointer 类型,就是对上文的 _Ptr 结构体中的 type 类型的使用,来取指针类型。然后是三个构造函数,一个是默认的空参构造,用了 C++11 中的default关键字;一个是仅传指针的构造函数,调用 _M_ptr 进行赋值;最后一个是传入指针和删除器的构造函数,赋值至成员变量 _M_t 中,这里用了 std::forward实现完美转发,也就是说这里的删除器可以是个右值。
C/C++学习记录:智能指针 std::unique_ptr 源码分析_第7张图片
  然后是几个取成员变量内容的方法和一个swap函数。一共是4个取成员变量内容的方法,指针和删除器各两个。两个方法中上面一个是取引用,即可以改变内容的;下面一个是const修饰的,是不可改内容的,应该是只用于查看内容。下面那个swap函数里,即为使用 std::swap 交换 __uniq_ptr_impl 类中的内容,此函数作用于 std::unique_ptr 中的指针所有权转让的情景。
C/C++学习记录:智能指针 std::unique_ptr 源码分析_第8张图片
  最后就是 __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/C++学习记录:智能指针 std::unique_ptr 源码分析_第9张图片
  • 定义二:
    C/C++学习记录:智能指针 std::unique_ptr 源码分析_第10张图片
      下面两个即为这两个删除器定义不同的最关键的地方。可以看到其中都存在 C++11 中的新特性静态断言,来进行合法性判定。而关键语句一个为 delete __ptr(85行),另一个为 delete[] __ptr(122行)。这即为默认删除器的核心语句,即单个对象通过delete释放,数组对象通过delete[]来释放。这里的实现是通过重载操作符 () 来实现的,算是一种仿函数的写法。
    C/C++学习记录:智能指针 std::unique_ptr 源码分析_第11张图片
    C/C++学习记录:智能指针 std::unique_ptr 源码分析_第12张图片

4. 回归 class unique_ptr

  通过上面对储存类和删除器的实现分析,已经大概了解了 std::unique 中的大部分封装好的内容。接下来就看一下在 class unique_ptr 中都有些什么东西。(此处仅记录重点内容)

  首先是若干种构造函数:

  1. 空参构造
    C/C++学习记录:智能指针 std::unique_ptr 源码分析_第13张图片
  2. 带参构造一,参数仅有指针
    C/C++学习记录:智能指针 std::unique_ptr 源码分析_第14张图片
  3. 带参构造二,参数为指针和其删除器
    C/C++学习记录:智能指针 std::unique_ptr 源码分析_第15张图片
  4. 带参构造三,传参为指针和删除器,但是都是右值
    C/C++学习记录:智能指针 std::unique_ptr 源码分析_第16张图片
  5. 带参构造四,传参为一个空指针
    5
  6. 带参构造五,传参为一个 std::unique 对象,这里涉及到引用折叠和完美转发,来实现所有权转移的操作
    6
  7. 带参构造六,传参为一个 std::unique 对象,相比上文那个更加严谨
    C/C++学习记录:智能指针 std::unique_ptr 源码分析_第17张图片

  接下来是析构函数,如下图只有一种实现(单对象和数组对象都是这个)。可以看到逻辑很简单就是在析构的时候调用仿函数删除器来进行delete操作。这里的 pointer 是获取空指针用的。
C/C++学习记录:智能指针 std::unique_ptr 源码分析_第18张图片
  剩下的内容中,大部分都是对操作符 =*->等的重载来方便设定智能指针的内容,以及方便使用其内容。另外还封装了几个函数如下,来获取成员变量 _M_t 中的指针和删除器,上面提到的若干重载也是基于下面这几个函数来实现的。另外还有一些基础的函数,例如清空智能指针用的 release() 或者 reset(),还有交换内容的 swap 函数,这几个就不多说了,因为实现都比较简单。
C/C++学习记录:智能指针 std::unique_ptr 源码分析_第19张图片
  在定义的最后还有两个被禁止的操作,即对 std::unique_ptr 进行左值赋值操作,从而来保证逻辑的正确执行。
3

三、总结

  总而言之,std::unique 的主要思路还是比较简单的。但是在标准库源码中会有很多很细节的操作,所以还是有很大的收获。在源码的最后还有若干对 hash 相关的重载操作,在此文中就不进行记录了。
  标准库的代码还是像套娃一样有很多层,前面看着会跳来跳去,但是了解底层以后,上面也就很清晰了。在接下来我应该会去看一下 std::shared_ptr 的实现,相对 std::unique_ptr 其肯定会复杂很多,但是这次搞完以后应该看起来也会轻松一点吧,毕竟思路还是有些相同点的,我的重点应该会放在其内部计数器的实现。

你可能感兴趣的:(C/C++,C++源码分析,c语言,c++,标准库,c++11,源码)