菜鸟笔记
提升您的技术认知

智能指针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;

测试过程

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

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的时候也演示过,所以此处就不花过多的篇幅举例了,这里写到一个测试函数中,体会一下用法就好:

    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的对象直接赋值的话,会在编译期间就提示错误:

    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(),但是作为返回值却不需要,这里留个疑问,最后分析一下:

    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在内在这种用法的情况下都不会发生问题:

    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));这一句,是不是感觉很神奇,居然不报编译错误,我感觉和作为返回值时是相同的处理。

    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对象的“删除器”,如果不手动指定就会获得默认的删除器,否则就返回你指定的,举个例子一看就明白了,代码如下:

    // 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(输出内容)
    
     // 调用get_deleter
     unique_ptr<Example, custom_deleter> ptr7(new Example(7), ptr6.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用法