unique_ptr浅析

前言

unique_ptr这个指针是C++11标准时被引入标准库的,有一种说法称它是boost::scoped_ptr的一个分身,并且它在C++11的时候“转正”了,但是scoped_ptr还被留在boost库中,看来没有转正的机会了,不过unique_ptrscoped_ptr确实很像,unique_ptr只比scoped_ptr多了一个移动语义,可以通过std::move()函数来转移内部对象的所有权。

其实在我看来,unique_ptrauto_ptr是最像的,他设计之初就是为了替代auto_ptr,其实两者基本上没有区别,如果把auto_ptr限制一下,使其不能通过拷贝构造和赋值获得所有权,但是可以通过std::move()函数获得所有权,那基本上就变成了unique_pr,这一点通过下面的函数分析也可以看出,两者的函数基本一致。

unique_pr作为一个模板类,可以直接用它来定义一个智能指针的对象,例如std::unique_pr<Test> pa(new Test);,查看unique_pr的代码时发现,它主要有getreleaseresetoperator*operator->operator=swapoperator boolget_deleter几个函数,相比于auto_ptr常用函数来说,只多了swapoperator boolget_deleter这三个函数,基本上没什么变化,不过get_deleter这个函数值的详细解释一下,下面通过一些例子来了解一下unique_pr的具体用法。

使用环境

  1. VS2015 + Windows7(应该是C++11标准)
  2. 头文件#include <memory>
  3. 命名空间using namespace std;

测试过程

首先我们先编写一个测试类,用来测试智能指针各个函数的作用,以及可能出现的问题,测试类的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Example
{
public:
Example(int param = 0)
{
number = param;
cout << "Example: " << number << endl;
}

~Example() { cout << "~Example: " << number << endl; }

void test_print() { cout << "in test print: number = " << number << endl; }

void set_number(int num) { number = num; }

private:
int number;
};

  1. 测试函数getreleaseresetoperator*operator->swapoperator bool
    这些函数在解释auto_ptr的时候基本都提到过,swapoperator bool作为两个新的函数在解释shared_ptr的时候也演示过,所以此处就不花过多的篇幅举例了,这里写到一个测试函数中,体会一下用法就好:

    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
    void test1()
    {
    unique_ptr<Example> ptr1(new Example(1)); // Example: 1(输出内容)
    if (ptr1.get()) // 调用get函数,判断内部指针的有效性
    {
    ptr1.get()->test_print(); // in test print: number = 1(输出内容)
    ptr1->set_number(2); // 调用了operator->
    (*ptr1).test_print(); // in test print: number = 2(输出内容)
    }

    if (ptr1) // 调用operator bool 检测内部对象的有效性
    cout << "ptr1 is valid\n"; // ptr1 is valid(输出内容)

    Example *p = ptr1.release(); // 调用release函数,取出内部对象
    if (!ptr1) // 调用operator bool 检测内部对象的有效性
    cout << "ptr1 is invalid\n"; // ptr1 is invalid(输出内容)

    ptr1.reset(p); // 调用reset函数,重新设置内部对象
    if (ptr1) // 调用operator bool 检测内部对象的有效性
    cout << "ptr1 is valid\n"; // ptr1 is valid(输出内容)

    ptr1->test_print(); // in test print: number = 2(输出内容)
    unique_ptr<Example> ptr2(new Example(20)); // Example: 20(输出内容)

    ptr1.swap(ptr2); // 调用swap函数,重新设置内部对象
    ptr1->test_print(); // in test print: number = 20(输出内容)
    ptr2->test_print(); // in test print: number = 2(输出内容)

    ptr1.reset(); // ~Example: 20(输出内容)// 重置内部对象被销毁
    } // ~Example: 2(输出内容) // 出作用域被析构
  2. 测试函数operator=
    operator=这个函数是unique_ptrauto_ptr最大的区别,因为在auto_ptr中,这个操作函数往往是导致问题出现的罪魁祸首,赋值之后所有权转移,原智能指针对象无效,这样往往会导致程序崩溃,所以在unique_ptroperator=被禁止使用了,取而代之的是具有移动语义的std::move()函数,如果unique_ptr的对象直接赋值的话,会在编译期间就提示错误:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void test2()
    {
    //unique_ptr<Example> ptr2 = new Example(2);// 编译错误,不支持原始指针到智能指针的隐式转换
    unique_ptr<Example> ptr2(new Example(2)); // Example: 2(输出内容)

    //unique_ptr<Example> ptr3 = ptr2; // 编译错误,...: 尝试引用已删除的函数
    //unique_ptr<Example> ptr4(ptr2); // 编译错误,...: 尝试引用已删除的函数
    unique_ptr<Example> ptr5(std::move(ptr2)); // 正常编译,使用move移动语义,符合预期效果
    ptr5->test_print(); // in test print: number = 2(输出内容)
    } // ~Example: 2(输出内容) // 出作用域被析构
  3. 测试unique_ptr作为参数和返回值
    unique_ptr是可以作为参数和返回值的,不过因为operator=不允许使用,所以在作为参数的时候需要使用函数std::move(),但是作为返回值却不需要,这里留个疑问,最后分析一下:

    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
    void test3_inner1(unique_ptr<Example> ptr3_1)
    {
    ptr3_1->test_print(); // in test print: number = 3(输出内容)
    } // ~Example: 3(输出内容) // 出作用域被析构

    unique_ptr<Example> test3_inner2()
    {
    unique_ptr<Example> ptr3_2(new Example(32));// Example:32(输出内容)
    ptr3_2->test_print(); // in test print: number = 32(输出内容)
    return ptr3_2;
    }

    void test3()
    {
    unique_ptr<Example> ptr3(new Example(3)); // Example:3(输出内容)
    ptr3->test_print(); // in test print: number = 3(输出内容)

    //test3_inner1(ptr3); // 直接作为参数传递会报编译错误,不存在拷贝构造
    test3_inner1(std::move(ptr3)); // 但是可以使用std::move的移动语义来实现

    if (!ptr3)
    cout << "ptr3 is invalid\n"; // ptr1 is valid(输出内容),移动之后ptr3无效

    ptr3 = test3_inner2(); // 由于不允许调用构造或者赋值,此处使用了移动语义move
    ptr3->test_print(); // in test print: number = 32(输出内容)
    } // ~Example: 32(输出内容),出定义域ptr3释放内部对象
  4. 测试unique_ptr类型的指针或者引用作为参数
    这一点没有什么问题,因为不会发生所有权的转移和引用计数的增加,所有的智能指针,包括auto_ptr在内在这种用法的情况下都不会发生问题:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    void test4_inner1(unique_ptr<Example>* ptr4_1)
    {
    (*ptr4_1)->test_print(); // in test print: number = 4(输出内容)
    } // 指针传递没有析构

    void test4_inner2(unique_ptr<Example>& ptr4_2)
    {
    ptr4_2->test_print(); // in test print: number = 4(输出内容)
    } // 引用传递没有析构

    void test4()
    {
    unique_ptr<Example> ptr4(new Example(4)); // Example:4(输出内容)
    ptr4->test_print(); // in test print: number = 4(输出内容)

    test4_inner1(&ptr4); // 取地址作为参数
    test4_inner2(ptr4); // 引用作为参数
    } // ~Example: 4(输出内容),出定义域ptr4释放内部对象
  5. 测试unique_ptr作为容器元素
    前面分析auto_ptr的时候已经说过,auto_ptr在作为容器元素时,是不具有跨平台性质的,因为在有的平台表现很正常,有的环境下直接编译报错,原因就是使用auto_ptr很容易出错,不是说一定会出错,而是可能出问题,所以个别平台直接在编译期报错,防止后续的错误。而unique_ptr作为容器元素时,表现很统一,没有任何问题,但是我感觉这里就有点牵强,后续再说,注意v[6] = unique_ptr<Example>(new Example(56));这一句,是不是感觉很神奇,居然不报编译错误,我感觉和作为返回值时是相同的处理。

    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
    void test5()
    {
    vector<unique_ptr<Example>> v(7);
    for (int i = 0; i < 6; i++)
    {
    v[i] = unique_ptr<Example>(new Example(50 + i)); // 依次输出Example:70,...Example:75
    }

    // 直接赋值,迷之成功,不是不能operator=吗,这里实际上调用的还是std::move类似的移动语义?
    v[6] = unique_ptr<Example>(new Example(56));// Example:56(输出内容)

    // 直接将unique_ptr对象push_back
    v.push_back(unique_ptr<Example>(new Example(57))); // Example:57(输出内容)

    // 利用移动语义push_back
    v.push_back(std::move(unique_ptr<Example>(new Example(58)))); // Example:58(输出内容)

    // 利用make_unique创建unique_ptr,C++14才支持
    v.push_back(make_unique<Example>(59)); // Example:59(输出内容)


    // 循环调用
    for (int i = 0; i < 10; i++)
    {
    v[i]->test_print();
    }// 依次输出in test print: number = 50....in test print: number = 59

    }// 依次输出~Example: 50,~Example: 51...~Example: 59
  6. 测试函数get_deleter
    这个函数还是第一次提到,作用就是获得unique_ptr对象的“删除器”,如果不手动指定就会获得默认的删除器,否则就返回你指定的,举个例子一看就明白了,代码如下:

    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
    // a custom deleter
    class custom_deleter {
    int flag;
    public:
    custom_deleter(int val) : flag(val) {}

    template <class T>
    void operator()(T* p)
    {
    std::cout << "use custom deleter, flag=" << flag ;
    delete p;
    }
    };

    void test6()
    {
    custom_deleter dlter(666);

    unique_ptr<Example, custom_deleter> ptr6(new Example(6), dlter); // Example:6(输出内容)
    ptr6->test_print(); // in test print: number = 6(输出内容)

    unique_ptr<Example, custom_deleter> ptr7(new Example(7), ptr6.get_deleter()); // 调用get_deleter

    // 重置智能指针,内部对象使用自定义删除器删除
    ptr6.reset(); // 输出:use custom deleter, flag = 666~Example: 6
    ptr7->test_print(); // in test print: number = 7(输出内容)
    } // 输出:use custom deleter, flag = 666~Example: 7

现象分析

上面的几个例子都很简单,基本上看一遍就知道怎么用了,但是有一点让人很迷惑,就是operator=的使用,最开始已经说过了,unique_ptr中的operator=已经被禁止使用了,但是例子中有两处很有争议,就是unique_ptr作为函数返回值和直接把unique_ptr赋值给vector元素,一开始我也不是太清楚,后来找资料时发现了一些线索,和大家分享一下:

当函数返回一个对象时,理论上会产生临时变量,那必然是会导致新对象的构造和旧对象的析构,这对效率是有影响的。C++编译针对这种情况允许进行优化,哪怕是构造函数有副作用,这叫做返回值优化(RVO),返回有名字的对象叫做具名返回值优化(NRVO),就那RVO来说吧,本来是在返回时要生成临时对象的,现在构造返回对象时直接在接受返回对象的空间中构造了。假设不进行返回值优化,那么上面返回unique_ptr会不会有问题呢?也不会。因为标准允许编译器这么做:
1.如果支持move构造,那么调用move构造。
2.如果不支持move,那就调用copy构造。
3.如果不支持copy,那就报错吧。

很显然,unique_ptr本身是支持move构造的,所以unique_ptr对象可以被函数返回,另外我推测将unique_ptr直接赋值给vector元素也利用了相似的操作,这里不太确定,希望了解的小伙伴能告知一下其中的原因。

说到这里,我们对unique_ptr也有了整体的认识,说unique_ptrauto_ptr的替代品,可是unique_ptr真的优秀了吗?
我看未必,它并非不会再犯错,只是犯错的成本大了一些,如果使用std::move()转移了所有权之后,再直接使用原来的智能指针对象,同样会使得程序崩溃。

其实auto_ptrunique_ptr给我的感觉就是就好比租房子,租房时有些人喜欢看一下房东的房产证,有的人则无所谓,来个人说是房东他就敢跟人签合同,房屋所有权是通过房产证来转移的,使用auto_ptr就好像两个人可以私下交易,把钱和房产证直接交换,房产证的转移很随便,使用unique_ptr就好比在转移房产的时候需要放鞭炮、然后在全世界广播一下,比较麻烦,并且有可能被租房的人看到,但是本质是一样的,都是拿钱来转移房的所有权,关键还是看租房的人,如果租房先看房产证,即使是房产证的转移很随便(也就是使用auto_ptr),也不会出问题,如果租房根本不看房产证,即使房产证交易通知了世界上所有人(即使用unique_ptr),也会租到没证的房子(程序崩溃)。

所以说unique_ptr并没有消除错误,仅仅是提高了犯错的成本。

总结

  1. 对比auto_ptrunique_ptr后发现,unique_ptr几乎只是将auto_ptroperator=改为std::move()函数。
  2. 现在标准库中只剩下了shared_ptrweak_ptrunique_ptr三个智能指针,weak_ptr是为了解决shared_ptr的循环引用问题而存在的,有其特定的使用情况,所以只剩下了shared_ptrunique_ptr的选择,选择的标准就是看是否需要对原对象共享所有权,如果需要使用shared_ptr,如果不需要是独占所有权的使用unique_ptr
  3. unique_ptr并没有从根本上消除可能错误,仅仅是提高了犯错的成本,并且给出移动所有权的提示,但是在容器vector元素赋值时依然很隐晦,可能造成auto_ptr相同的错误。

测试源码

示例传送门:unique_ptr用法

Albert Shi wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客