C++ STL笔记02-std::unique_ptr独占型智能指针

std::unique_ptr用法

std::unique_ptr是C++ 11引入的一种智能指针,它的特点是独享被管理对象指针所有权:它不允许其他的智能指针共享其内部的指针, 不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr。当对象被销毁时,会在unique_ptr的析构函数中删除关联的原始指针。由于unique_ptr重载了->*运算符,它可以像通常的指针一样进行使用。

初始化

unique_ptr有以下几种初始化方式:

  • 裸指针直接初始化,但不能通过隐式转换来构造;
  • 允许移动构造,但不允许拷贝构造;
  • C++ 14后可以使用make_unique()函数进行构造,但需要注意使用这种方式进行构造无法添加删除器或者初始化列表。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <memory>

struct MyClass {};

int main()
{   
    std::unique_ptr<MyClass> p(new MyClass());                  // 裸指针直接初始化
    // std::unique_ptr<MyClass> p1 = new MyClass();             // Error, explicit禁止隐式初始化
    // std::unique_ptr<MyClass> p2(p);                          // Error, 禁止拷贝构造函数
    // std::unique_ptr<MyClass> p3 = p;                         // Error, 禁止拷贝构造函数

    // std::unique_ptr<MyClass> p3;
    // p3 = p;                                                  // Error,禁止拷贝赋值运算符重载
    
    std::unique_ptr<MyClass> p4(std::move(new MyClass()));      // 移动构造函数
    // std::unique_ptr<MyClass> p5 = std::move(new MyClass());  // Error, explicit禁止隐式初始化
    std::unique_ptr<MyClass> p6(std::move(p4));                 // 移动构造函数
    std::unique_ptr<MyClass> p7 = std::move(p6);                // 移动赋值运算符重载
    
    std::unique_ptr<MyClass[]> p8(new MyClass[10]());           // unique_ptr指向数组
    
    std::unique_ptr<MyClass> p9 = std::make_unique<MyClass>();  // C++14后使用std::make_unique进行构造

    return 0;
}

删除器

unique_ptr在构造时会引入一个删除器,它的作用指定是在unique_ptr对象被销毁时析构函数中释放关联的原始指针的方式。当不指定删除器时编译器会自动指定为默认删除器std::default_delete。删除器可以是函数指针,也可以是lambda表达式或者模板。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
#include <memory>

struct MyClass {};

void MyDeleter (MyClass *p) {
    printf("[MyDeleter called]\n");
    delete p;
}

template <typename T>
struct MyDeleterTemplate {
    void operator() (T *p) {
        printf("[MyDeleterTemplate called]\n");
        delete p;
    }
};

int main()
{   
    {
        std::unique_ptr<MyClass, decltype(&MyDeleter)> p(new MyClass(), MyDeleter);
    }
    
    {
        auto MyDeleterLambda = [](MyClass *p) {
            printf("[MyDeleterLambda called]\n");
            delete p;
        };
        
        std::unique_ptr<MyClass, decltype(MyDeleterLambda)> p(new MyClass(), MyDeleterLambda);
    }
    
    {
        auto deleter = MyDeleterTemplate<MyClass>();
        std::unique_ptr<MyClass, MyDeleterTemplate<MyClass>> p(new MyClass(), deleter);

    }

    return 0;
}

运行上面的代码可以得到如下结果:

1
2
3
[MyDeleter called]
[MyDeleterLambda called]
[MyDeleterTemplate called]

常用操作

除了上面介绍的内容外,unique_ptr还有以下常用操作:

  • get():返回unique_ptr中保存的裸指针;
  • reset():重置unique_ptr
  • release():放弃对指针的控制权,返回裸指针,并将unique_ptr自身置空(需要注意,此函数放弃了控制权但不会释放内存,如果不获取返回值,就会丢失指针并造成内存泄露);
  • swap():交换两个unique_ptr所指向的对象;

UniquePtr实现

接下来我们从零开始实现一个类似于标准库中std::unique_ptr的指针UniquePtr

默认删除器

首先我们来实现一个默认删除器,它通过重载函数调用运算符来释放UniquePtr内的指针。

1
2
3
4
5
6
template <class T>
struct DefaultDeleter {
    void operator()(T *p) const {
        delete p;
    }
};

对于数组的情形,我们引入一个模板偏特化来进行处理。

1
2
3
4
5
6
template <class T>
struct DefaultDeleter<T[]> { // 偏特化
    void operator()(T *p) const {
        delete[] p;
    }
};

除此之外,对于文件类型我们需要在删除器中关闭文件,这里使用模板全特化来实现。

1
2
3
4
5
6
template <>
struct DefaultDeleter<FILE> { // 全特化
    void operator()(FILE *p) const {
        fclose(p);
    }
};

完成默认删除器DefaultDeleter后我们就可以实现UniquePtr的定义和析构函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
template <class T, class Deleter = DefaultDeleter<T>>
struct UniquePtr {
private:
    T *m_p;

public:
    ...
    ~UniquePtr() { // 析构函数
        if (m_p)
            Deleter{}(m_p);
    }
    ...
}

而对于数组类型的模板参数T[]则可以通过片特化来实现。

1
2
template <class T, class Deleter>
struct UniquePtr<T[], Deleter> : UniquePtr<T, Deleter> {};

构造函数

接下来我们来实现UniquePtr的构造函数。在C++中一共有五种构造函数,包括默认构造函数、参数化构造函数、拷贝构造函数、移动构造函数以及委托构造函数。很多时候我们只需要实现其中的一部分并把剩下的交给编译器来帮我们实现即可,不过对于UniquePtr我们需要逐一实现它们。

默认构造函数

默认构造函数是没有参数的构造函数,它用于创建对象时不需要提供任何参数。此时我们需要把UniquePtr中的m_p指针设置为空指针nullptr

1
2
3
4
5
6
7
8
template <class T, class Deleter = DefaultDeleter<T>>
struct UniquePtr {
public:
    UniquePtr(std::nullptr_t dummy = nullptr) {
        m_p = nullptr;
    }
    ...
}

参数化构造函数

参数化构造函数接受一个或多个参数,用于初始化对象的成员变量。这里我们利用构造函数把p指针赋值给UniquePtr中的m_p指针进行初始化。

1
2
3
4
5
6
7
8
9
template <class T, class Deleter = DefaultDeleter<T>>
struct UniquePtr {
public:
    ...
    explicit UniquePtr(T *p) {
        m_p = p;
    }
    ...
}

上面代码中的explicit关键字表示这个构造函数是显式的,它不允许隐式类型转换。这是为了确保只有在明确传递指针参数的情况下才能创建UniquePtr对象,而不会发生意外的类型转换。

拷贝构造函数

拷贝构造函数使用同一类的另一个对象来初始化自身,它通常用来处理对象拷贝的行为。不过对于UniquePtr我们需要禁止拷贝的行为,这里使用delete关键字来禁用拷贝构造函数。

1
2
3
4
5
6
7
template <class T, class Deleter = DefaultDeleter<T>>
struct UniquePtr {
public:
    ...
    UniquePtr(UniquePtr const &that) = delete;
    ...
}

类似地,我们同样需要禁用UniquePtr的拷贝赋值运算符:

1
2
3
4
5
6
7
template <class T, class Deleter = DefaultDeleter<T>>
struct UniquePtr {
public:
    ...
    UniquePtr &operator=(UniquePtr const &that) = delete;
    ...
}

移动构造函数

尽管UniquePtr不支持拷贝行为,我们仍然需要实现它的移动行为。首先我们定义一个辅助函数exchange,它类似于标准库中的std::exchange函数。

1
2
3
4
5
6
template <class T, class U>
T exchange(T &dst, U &&val) {
    T tmp = std::move(dst);
    dst = std::forward<U>(val);
    return tmp;
}

在此基础上就可以实现UniquePtr的移动构造函数以及移动赋值运算符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template <class T, class Deleter = DefaultDeleter<T>>
struct UniquePtr {
public:
    ...
    UniquePtr(UniquePtr &&that) {
        m_p = exchange(that.m_p, nullptr);
    }

    UniquePtr &operator=(UniquePtr &&that) {
        if (this != &that) [[likely]] {
            if (m_p)
                Deleter{}(m_p);
            m_p = exchange(that.m_p, nullptr);
        }
        return *this;
    }
    ...
}

需要说明的是在移动赋值的情况下,如果当前指针非空则要在赋值前先利用删除器释放掉自身的指针,然后再进行赋值。

为了让UniquePtr支持类型转换,我们需要再定义一个构造函数将UniquePtr<U>转换为UniquePtr<T>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <class T, class Deleter = DefaultDeleter<T>>
struct UniquePtr {
private:
    ...
    template <class U, class UDeleter>
    friend struct UniquePtr;

public:
    ...
    template <class U, class UDeleter> requires (std::convertible_to<U *, T *>)
    UniquePtr(UniquePtr<U, UDeleter> &&that) {  // 从子类型U的智能指针转换到T类型的智能指针
        m_p = exchange(that.m_p, nullptr);
    }
    ...
}

注意这里首先通过friend令所有UniquePtr<T>UniquePtr<U>互为友元以便访问对方的private成员;并且在构造函数中通过std::convertible_to函数要求U类型指针能够转换为T类型指针,满足要求后再通过exchange函数进行转换和构造。

其它函数

UniquePtr的其它常用成员函数可以参考如下实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
template <class T, class Deleter = DefaultDeleter<T>>
struct UniquePtr {
public:
    ...
    T *get() const {
        return m_p;
    }

    T *release() {
        return exchange(m_p, nullptr);
    }

    void reset(T *p = nullptr) {
        if (m_p)
            Deleter{}(m_p);
        m_p = p;
    }

    T &operator*() const {
        return *m_p;
    }

    T *operator->() const {
        return m_p;
    }
}

为了便于构造UniquePtr指针,我们再额外编写一个makeUnique()函数:

1
2
3
4
template <class T, class ...Args>
UniquePtr<T> makeUnique(Args &&...args) {
    return UniquePtr<T>(new T(std::forward<Args>(args)...));
}

如果只需要调用类型T的默认初始化构造UniquePtr指针,则可以使用如下makeUniqueForOverwrite()函数,它类似于C++ 20后标准库中的std::make_unique_for_overwrite

1
2
3
4
template <class T>
UniquePtr<T> makeUniqueForOverwrite() {
    return UniquePtr<T>(new T);
}

完整实现

总结一下,整个UniquePtr的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#include <cstdio>
#include <utility>
#include <concepts>

template <class T>
struct DefaultDeleter { // 默认使用 delete 释放内存
    void operator()(T *p) const {
        delete p;
    }
};

template <class T>
struct DefaultDeleter<T[]> { // 偏特化
    void operator()(T *p) const {
        delete[] p;
    }
};

template <>
struct DefaultDeleter<FILE> { // 全特化
    void operator()(FILE *p) const {
        fclose(p);
    }
};

template <class T, class U>
T exchange(T &dst, U &&val) { // 同标准库的 std::exchange
    T tmp = std::move(dst);
    dst = std::forward<U>(val);
    return tmp;
}

template <class T, class Deleter = DefaultDeleter<T>>
struct UniquePtr {
private:
    T *m_p;

    template <class U, class UDeleter>
    friend struct UniquePtr;

public:
    UniquePtr(std::nullptr_t dummy = nullptr) { // 默认构造函数
        m_p = nullptr;
    }

    explicit UniquePtr(T *p) { // 自定义构造函数
        m_p = p;
    }

    // template <class U, class UDeleter, class = std::enable_if_t<std::is_convertible_v<U *, T *>>> // 没有 C++20 的写法
    template <class U, class UDeleter> requires (std::convertible_to<U *, T *>) // 有 C++20 的写法
    UniquePtr(UniquePtr<U, UDeleter> &&that) {  // 从子类型U的智能指针转换到T类型的智能指针
        m_p = exchange(that.m_p, nullptr);
    }

    ~UniquePtr() { // 析构函数
        if (m_p)
            Deleter{}(m_p);
    }

    UniquePtr(UniquePtr const &that) = delete; // 拷贝构造函数
    UniquePtr &operator=(UniquePtr const &that) = delete; // 拷贝赋值函数
    
    UniquePtr(UniquePtr &&that) { // 移动构造函数
        m_p = exchange(that.m_p, nullptr);
    }
    
    UniquePtr &operator=(UniquePtr &&that) { // 移动赋值函数
        if (this != &that) [[likely]] {
            if (m_p)
                Deleter{}(m_p);
            m_p = exchange(that.m_p, nullptr);
        }
        return *this;
    }

    T *get() const {
        return m_p;
    }

    T *release() {
        return exchange(m_p, nullptr);
    }

    void reset(T *p = nullptr) {
        if (m_p)
            Deleter{}(m_p);
        m_p = p;
    }

    T &operator*() const {
        return *m_p;
    }

    T *operator->() const {
        return m_p;
    }
};

template <class T, class Deleter>
struct UniquePtr<T[], Deleter> : UniquePtr<T, Deleter> {};

template <class T, class ...Args>
UniquePtr<T> makeUnique(Args &&...args) {
    return UniquePtr<T>(new T(std::forward<Args>(args)...));
}

template <class T>
UniquePtr<T> makeUniqueForOverwrite() {
    return UniquePtr<T>(new T);
}

Reference