Effective Modern C++ 条款18 用std::unique_ptr管理独占所有权的资源

用std::unique_ptr管理独占所有权的资源

当你伸手触碰智能指针的时候,std::unique_ptr通常是最触手可及的一个。这样认定它是有道理的,默认情况下,std::unique_ptr的大小与原生指针相同,然后它的大部分操作(包括解引用),执行的指令与原生指针执行的指令相同。 这意味着在内存紧张和调度密集的情况下,你仍然可以使用它。如果你觉得原生指针又小又快,那么std::unique_ptr几乎也是这样。

std::unique_ptr表示独占所有权 语义。一个非空的std::unique_ptr会一直拥有它指向的对象。移动一个std::unique_ptr,所有权会从源指针转移到目的指针。(之后源指针会设置为空指针。)拷贝std::unique_ptr是不允许的,因为如果你可以拷贝它,那么就有两个std::unique_ptr指向相同的资源,每一个都认为它拥有(和负责销毁)那份资源。因此,std::unique_ptr是只可移动类型。当销毁的时候,一个非空的std::unique_ptr会销毁它的资源,默认情况下,资源销毁是通过对std::unique_ptr内的原生指针使用delete来完成的。

std::unique_ptr的一个常见使用是作为工厂函数的返回类型(在分层中)。假如我们有一个投资(investment)类型的分层(例如,包含股票stock,债券bond,房地产real estate,等等),基类是Inverstment:

这里写图片描述

class Investment { ... };

class Stock : public Investment { ... };

class Bond : public Investment { ... };

class RealEstate : public Investment { ... };

这种分层的工厂返回通常从堆上分配一个对象,然后返回一个指向它的指针,当不再需要它的时候,调用者要负责delete这个对象。那真是完美匹配std::unique_ptr,因为调用者需要为工厂返回的资源负责(即独占资源的所有权),然后std::unique_ptr在它销毁的时候可以自动delete它指向的对象。Investment分层的工厂函数应该这样声明:

template <typename... Ts>        // 返回一个由给定参数
std::unique_ptr<Investment>     // 创建的对象的指针
makeInvestment(Ts&&... params); 

调用者可以在一个局部作用域使用返回的std::unique_ptr,就像这样:

{                  
    ...
    auto pInvestment =                      // pInvestment的类型是
          makeInvestment(*arguments*);       // std::unique_ptr<Investment>
    ...
}    // 销毁 *pInvestment

不过它们也可以使用迁移语义,例如,工厂返回的std::unique_ptr被移入容器中,随后容器的元素移动到一个对象的成员变量中,最终对象会被销毁。当发生这种事时,对象的std::unique_ptr成员变量也会被销毁,然后它的销毁会导致资源的销毁。如果所有权传递链因为异常或非典型控制流(例如,函数过早的return或者循环过早的break)而中断,那么管理资源的std::unique_ptr最终还是会调用析构函数,随后销毁管理的资源。

默认情况下,std::unique_ptr是借助delete来销毁管理的资源,但是,在构造std::unique_ptr期间,你可以指定使用自定义的删除器:当std::unique_ptr销毁资源时调用的函数(或者函数对象,包括lambda表达式)。如果由makeInvestment创建的对象不应该直接delete,而是应该首先记录日志,那么makeInvestment可以这样实现(在代码后会有解释,所以不用担心看不懂代码):

auto delInvmt = [](Investment *pInvestment)   // 自定义的删除器
                {                                                  // 一个lambda表达式
                    makeLogEntry(pInvestment); 
                    delete pInvestment;
                };

template <typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt)>   // 修改返回类型
makeInvestment(Ts&&... params)
{`
    std::unique_ptr<Investment, decltypedelInvmt)>
       pInv(nullptr, delInvmt);             // 将要返回的指针
    if ( /* a Stock object should be created */ )
    {
        pInv.reset(new Stock(std::forward<Ts>(params)...));
    }
    else if (/* a Bond object should be created */) 
    {
        pInv.reset(new Bond(std::forward<Ts>(params)...));
    }
    else if ( /* a RealEstate obejct should be created */ )
    {
        pInv.reset(new RealEstate(std::forward<Ts>(params)...));
    }

    return pInv;
}

等下我会讲解它是怎样工作的,不过先来考虑在调用者看来,事情是怎样的。假定你用auto变量存储makeInvestment的结果,你对于你申请的资源在释放时的特殊处理毫不知情,却傻傻地欣然使用。事实上呢,你是可以沉浸在欢喜之中,因为使用std::unique_ptr意味着你不用关心资源什么时候销毁,更不用说资源销毁是一定会发生的。std::unique_ptr会自动处理好所有的事情,从用户的角度看,makeInvestment这个接口太爽了。

一旦你理解了下面的内容,你会觉得makeInvestment的实现也非常漂亮:

  • delInvmt是从makeInvestment返回对象的自定义删除器。所有的自定义删除函数都是接受一个指向需要销毁的对象的原生指针作为参数,然后它做的事情是销毁对象的必要工作。在这个例子中,删除器的行为是调用makelogEntry,然后应用delete。使用lambda表达式创建delInvmt是很方便的,不过我们很快就能看到,它还会比传统函数高效。
  • 当我们使用自定义的删除器的时候,它的类型要作为std::unique_ptr的第二个模板参数。在这个例子中,那是delInvmt的类型,这也是为什么makeInvestment的返回类型是std::unique_ptr<Investment, decltype(delInvmt)>。(关于decltype,看条款3。)
  • makeInvestment的基本策略是先创建一个空的std::unique_ptr,然后指向一个类型合适的对象,然后返回它。为了关联删除器delInvmtpInvmt,我们把删除器作为第二个构造参数传递给std::unique_ptr
  • 试图将原生指针(例如new出来的指针)赋值给std::unique_ptr是不会通过编译的,因为这导致一个从原生指针到智能指针的隐式转换,这样的隐式转换是有问题的,所以C++11的智能指针禁止这样的转换。那就是为什么reset被用来——让pInvmt得到new出来的对象的所有权。
  • 对于每个new,我们都用std::forward来完美转发makeInvestment的参数。这样的话,创建对象的构造函数能得到调用者提供的所有的信息。
  • 自定义删除器的参数类型是Investment *。不管makeInvestment函数实际创建的对象是什么类型(例如,Stock,Bond,RealEstate),它最终都会在lambda表达式里作为一个Investment *对象被删除。这意味着我们可以通过基类指针删除派生类对象。为了实现这项工作,基类——Investment——必须要有一个虚析构函数:
class Investment {
public:
    ...
    virtual ~Investment();`  // 设计的基本组成成分
    ...
};

在C++14,函数返回类型推断(条款3)意味着makeInvestment可以实现得更简单和更具有封装性:

template <typename... Ts>
auto makeInvestment(Ts&&... params)   // C++14
{
   auto delInvmt = [](Investment* pInvestment) // 自定义删除器
                   {                                                // 内置于makeInvestment
                       makeLogEntry(pInvestment);
                       delete pInvestment;
                   };

    std::unique_ptr<Investment, decltype(delInvmt)> 
      pInv(nullptr, delInvmt); 
                                            `
    if (...)
    {
        pInv.reset(new Stock(std::forward<Ts>(params)...));
    }
    else if (...)
    {
        pInv.reset(new Bond(std::forward<Ts>(params)...));
    }
    else if (...)
    {
        pInv.reset(new RealEstate(std::forward<Ts>(params)...));
    }
    return pInv;
}

我在之前说过,当你使用默认删除器时(即delete),你有理由假定std::unique_ptr对象的大小和原生指针一样。当自定义删除器出现后,就不是这样了。如果删除器是函数指针,它通常会让std::unique_ptr的大小增加一到两个字(word)。如果删除器是函数对象,std::unique_ptr的大小改变取决于函数对象存储了多少状态。无状态的函数对象(例如,不捕获变量lambda表达式)不会受到一丝代价,这意味着当自定义删除器即可用函数实现又可用不捕获变量的lambda表达式实现时,lambda实现会更好:

auto delInvmt1 = [](Investment* pInvestment)           // 自定义删除器是
                 {     // 不捕获变量的
                     makeLogEntry(pInvestment);      // lambda表达式
                     delete pInvestment;
                 };

template <typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt11)>   // 返回类型的大小
makeInvestment(Ts&&... args);                      // 与Investment*相同

void delInvmt2(Investment* pInvestment)    // 自定义删除器是函数
{
    makeLogEntry(pInvestment);
    delete pInvestment;
}
                                    `
template <typename... Ts>                                                 // 返回类型大小为
std::unique_ptr<Investment, void (*)(Investment*)>   // Investment* 加上
makeInvestment(Ts&&... params);                      //  至少一个函数指针的尺寸

带有大状态的函数对象会造成很大的std::unique_ptr。如果你发现自定义删除器让你的std::unique_ptr大得不能接受,那么你很可能需要改变你的设计了。

std::unique_ptr的常见使用不只是工厂函数,更普遍的是作为一种实现Pimpl Idiom的技术。这种代码不难实现,但是不够直截了当,因此条款22我会专门讲它。

std::unique_ptr有两种形式,一种是单独的对象(std::unique_ptr<T>),另一种是数组(std::unique_ptr<T[]>)。这样的结果是,std::unique_ptr指向的实体类型决不会是含糊的。std::unique_ptr的设计可以精确匹配你使用的形式。例如,单独的对象形式是没有下标引用操作(operator[]),而数组形式没有解引用操作(operator*和operator->)。

std::unique_ptr的数组形式的知道就好啦,因为比起原生数组,std::arraystd::vetcorstd::string实际上更好的数据结构。我能想到的std::unique_ptr数组唯一有意义场景就是,当你使用C-like风格的API,并且这API返回一个指像数组的指针,数组是从堆分配的,你需要对这个数组负责。

在C++11中,std::unique_ptr是表达独占所有权的方式,但它最吸引人的一个特性是它能即简单又高效地转化为std::shared_ptr

std::shared_ptr<Investment> sp =    // 把 std::unique_ptr转换为
    makeInvestment(argument);         // std::shared_ptr

这是为什么std::unique_ptr如此适合做工厂函数的关键原因,工厂函数不会知道:独占所有权语义和共享所有权语义哪个更适合调用者。通过返回一个std::unique_ptr,工厂提供给调用者的是最高效的智能指针,但它不妨碍调用者用std::shared_ptr来替换它(关于std::shared_ptr的信息,看条款19)。


总结

需要记住的3点:

  • std::unique_ptr是一个智能,快速,只可移动的智能指针,它以独占所有权语义管理资源。
  • 默认情况下,通过delete来销毁资源,但可以指定自定义删除器。有状态的删除器和函数指针作为std::unique_ptr的删除器会增加std::unique_ptr对象的大小。
  • std::unique_ptr转换为std::shared_ptr是容易的。
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页