effective modern c++ 条款18 使用unique_ptr管理独占性资源

当你准备使用智能指针时,应该首先考虑unique_ptr。有理由认为,unique_ptr和原始指针一样大小,对于大部分操作(包括解引用),二者都执行几乎相同的指令。这意味着你甚至可以在内存和时钟周期都很紧张情况下使用它们。如果一个原始指针对你来说足够小也足够快,那么unique_ptr也如此。

    unque_ptr包含所有权管理,非空的unique_ptr总拥有它指向的对象。对unique_ptr的move操作,使所有权从源指针转移到了目的指针上。(源指针被置空) unque_ptr不允许拷贝操作,因为如果允许拷贝存在,那么两个unique_ptr会指向同一个资源,每一个都认为自己拥有资源所有权(以备将来销毁)。因此unique_ptr是一个只能move的类型。一个非空unique_ptr析构时会释放资源。默认情况下,由对包含的原始指针做delete操作,来完成释放资源。

  使用unique_ptr的一个常见例子是工厂模式的封装函数,可以返回指定类型的对象。假定我们有一个对投资品(股票,债卷和不动产)的封装,有一个投资品的基类:

class Investiment {...}

class Stock : public Investiment{...}

class Bond : public Investiment{...}

class RealEstate : public Investiment{...}

 

这类工厂方法通常在堆上分配一个对象,返回对象的指针,由调用者在使用完毕后释放资源。使用unique_ptr是很适合的,因为调用者需要资源所有权,而去除了工厂方法对资源的所有权, 而且unique_ptr在析构时会自动析构所指向的资源。投资封装的工厂方法可以这样声明:

template<typename ... Ts>

std::unique_ptr<Investiment>

makeInvestiment(Ts && ... param);

用户可以这样调用:

auto pInvestiment =

makeInvestiment(arguments);

它们也可以用在所有权发生迁移的场景。比如从工厂方法返回的unique_ptr移动到容器中去,又从容器中移动到一个对象的成员变量上去。当这个对象析构时,unique_ptr也会析构,从工厂获取的资源也就销毁了。如果所有权转移链被意外中断,比如产生了异常或者进入其他非典型控制流(函数提前返回或者从循环中退出),拥有所有权的unique_ptr会析构,管理的资源得到了释放。

   默认情况下,资源通过delete而释放。但在unque_ptr构造函数中,可以传一个用户自定义的删除器:函数(函数对象,以及lamda表达式生成的函数对象)。 当它们管理的资源释放时,这些函数就会被调用。设想makeInvestiment的创建的对象不应该直接delete,而应该先记录一个日志,可以按照下面方式来实现(代码后面由解释,你不用担心这些代码的意图不好理解)。

auto delInvestiment = [](Investiment *pInvestiment) {

    makeLogEntry(pInvestiment);

    delete pInvestiment;

};

template <typename ...Ts>

std::unique_ptr<Investiment,decltype(delInvestiment)>

makeInvestiment(Ts && ... param) {

std::unique_ptr<Investiment,decltype(delInvestiment)>

pInv(nullptr, delInvestiment); 

if (/*stock*/) {

  pInv.reset(new Stock(std::forward<Ts>(param)...));

} else if (/*Bond*/) {

  pInv.reset(new Bond(std::forward<Ts>(param)...));

}

return pInv;

}

马上我就要解释它如何工作的, 首先从调用者角度考虑一下事情看起来是什么样的。假定你把makeInvestiment的结果保存在auto变量中,你非常希望能忽略掉资源销毁时,必须要做的一些额外操作。事实上你可以松一口气了,因为使用unique_ptr你不用考虑什么时候销毁,更不用说在程序的所有路径中,确保销毁操作进行且仅进行一次。unique_ptr 负责照看上面的事务。从客户角度来讲,unique_ptr非常的甜心。

    一旦你理解了下面的解释,你会发现这个实现的也很漂亮

delInvestiment 是makeInvestiment返回的对象的删除器。所有的用户删除函数都接受一个指向删除对象的原始指针为参数,在函数内做删除对象的必须工作。这种情况下,指调用makeLogEntry 和delete。使用lamda  表达式来创建delInvestiment是方便的,我们接下来会看到,它比写一段传统函数要效率高。

当使用用户定义删除函数时,unique_ptr使用该函数类型作为第二个参数类型而进行特。在这个例子中,是delInvestiment的类型,这也是为什么makeInvestiment的返回值类型为std::unique_ptr<Investiment,decltype(delInvestiment)>.

makeInvestiment最基本策略是创建一个空的unique_ptr,让它指向正确类型的对象,然后返回它。为了使pInv与用户自定义删除器delInvestiment关联,我们将其作为构造函数的第二个参数来传递。

原始指针赋值给unique_ptr不能编译,因为这被算作是从原始指针到unique_ptr的隐式转换。这种隐式转换会造成问题,所以c++11的智能指针禁止了这种做法。这也是为什么pInv通过reset获取new创建对象的所有权。

在上面每个new的使用过程,我们使用了std::forward来实现参数完美转发给makeInvestiment。 这使得调用者的信息对于生成对象的构造函数可用。

用户的删除器接受一个类型为Investiment*的参数。不管实际对象类型是什么,在lamda表达式中,我们最终会将其作为一个Investiment对象删除。这表示我们要通过基类指针来删除派生类对象,因此基类Investiment的析构函数应该是虚函数。

public Investiment {

public:

    virtual ~Investiment();

};

在c++14存在函数返回值的自动推导(条款3),这意味着makeInvestiment可以实现的更为简洁和优雅。

template <typename ...Ts>

auto makeInvestiment(Ts && ... param) {

std::unique_ptr<Investiment,decltype(delInvestiment)>

pInv(nullptr, delInvestiment); 

if (/*stock*/) {

  pInv.reset(new Stock(std::forward<Ts>(param)...));

} else if (/*Bond*/) {

  pInv.reset(new Bond(std::forward<Ts>(param)...));

}

return pInv;

}

 

我之前提出过,使用默认的删除器时,可以有理由认为unique_ptr和原始指针有同样大小。但如果使用了自定义删除器,就不再是这样了。对于是函数指针的删除器,unique_ptr的长度就会从1个字变成2个字。对于是函数对象的删除器,长度改变取决于函数对象保存了多少状态数据。无状态数据的函数对象(无捕获lamda)不会引起长度增加。这意味着,当用户删除器可以由函数指针或者无捕获lamda时,后者更为可取。

使用保存过多状态的函数对象作为删除器时,unique_ptr的大小会显著增加。如果你发现某个用户自定义删除器使得你的unique_ptr变得不可接受的大了,这表示你可能要更改你的设计了。

工厂方法并不是unique_ptr唯一经常使用的用例。它们作为实现Pimpl Idiom的一个机制更为流行。代码不复杂,但不够直观,我会在条款22中介绍,条款22也是围绕这个主题的。

unique_ptr有两种形式,一种是单一对象(unique_ptr<T>),另外一种是数组形式(std::unique_ptr<T[]>)。unique_ptr API的设计与其形式相符合。比如单一对象的API没有索引操作(operator[]),而数组的API没有解引用操作(*,->)。

对于数组形式的unique_ptr,你最好只是把它当成一种智力兴趣。因为std::array,std::vector和std::string 与原始数组比较差不多总是好的选择。唯一我可以想到的数组应用是,当从C风格的API返回一个指向堆上的数组,你接管所有权。

unique_ptr 是c++11中独占所有权的指针。但它最吸引人的一个特征是,它可以简单而高效的转换成一个shared_ptr

std::shared_ptr<Investiment> sp =  // converts a unique_ptr

    makeInvestiment(arguments);     // to shared_ptr

这就是为什么工厂模式使用unique_ptr最合适了,因为工厂不知道用户需要独占还是共享所有权。返回uniique_ptr就提供了最高效的方式,同时也不妨碍用户将其转成最方便的方式(shared_ptr)。

注意事项:

unique_ptr是一个小而快的,只能move的智能指针。

默认使用delete完成资源释放,但可以自定义删除函数。使用含状态数据的函数对象和函数指针时会增加unique_ptr对象的大小。

将unique_ptr转换成shared_ptr非常容易。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值