Effective Modern C++翻译系列之Item18

来源:互联网 发布:mysql synonym 编辑:程序博客网 时间:2024/06/05 11:59

Item18:Use std::unique_str for exclusive-ownership resource management.

当你需要一个智能指针的时候,std::unique_ptr一般是最容易用到的一种。有理由假设,在默认情况下,std::unique_ptr和原生指针有相同的大小以及大部分操作符(包括解引用操作符),它们执行完全相同的指令。这意味着你可以在存储和循环频繁发生的情况下使用它们。如果一个原生指针足够小足够快,那么std::unique_ptr几乎也是这样的。

std::unique_ptr拥有资源独占语义。一个非空的std::unique_ptr总是拥有它指向的对象。移动一个std::unique_ptr会把所有权从源指针转移到目的指针(源指针指向null)拷贝一个std::unique_ptr不被允许,因为如果你能够拷贝一个std::unique_ptr,那么将会有两个std::unique_ptr指向相同的资源,每个指针都认为它拥有(也应该销毁)该资源。因此std::unique_ptr是一个仅可以移动的类型。在析构函数中,一个非空的std::unique_ptr销毁它的资源。缺省情况下,资源的销毁是通过delet std::unique_ptr中的原生指针实现的。

std::unique_ptr的一个普遍应用是作为工厂函数的返回类型(指向类层次中的对象)。假设我们有一个以Investment为基类的代表着投资(例如股票,债券,不动产等等)的类层次。

class Investment { ... };

 

class Stock:

public Investment { ... };

 

class Bond:

public Investment { ... };

 

class RealEstate:

public Investment { ... };

 

对于这样的类层次,工厂函数常常会在堆上分配一个对象并且返回一个指向它的指针,调用者有责任在不需要该对象的时候销毁它。这种情况和std::unique_ptr完美匹配,因为调用者获得了从工厂函数返回的资源的管理责任(即对它的独自占有权),std::unique_ptr自动delete它指向的对象(当std::unique_ptr对象被销毁的时候)。对于这样的类层次,一个工厂函数可能被声明为这样:

 

template<typename... Ts>

std::unique_ptr<Investment> makeInvestment(Ts&&... params);

调用者能在一个作用域中像下面这样使用所返回的std::unique_ptr

{

...

auto pInvestment = //pInvestment的类型是

makeInvestment( arguments ); //std::unique_ptr<Invsetment>

...

}

 

但是它们还可以被用于所有权转移的情况,比如说当从工厂函数返回的std::unique_ptr被移动到一个容器里,容器中的元素接着被移动到一个对象的数据成员中,该对象随后被销毁了。当这件事发生时,对象的std::unique_ptr数据成员也会被销毁,它的析构函数会造成工厂函数返回的资源被销毁。如果因为异常或者其他非正常的控制流,所有权链被打断(例如函数中过早return或者循环中break出来),该std::unqiue_ptr拥有着管理的资源,最终会调用它的析构函数,它管理的资源也最终被销毁。

缺省情况下,析构函数中会发生delete操作,但是,在构造过程中,std::unique_ptr对象可以被设置自定义的deleter:当资源被销毁的时候,任意的自定义函数(或者仿函数,包括lambda表达式产生的仿函数)会被调用。如果makeInvestment产生的对象不应该直接被delete,而是首先写日志记录,makeInvestment能够被实现成如下这样。(代码后面会有解释,所以如果你有些部分看不懂,不用担心。)

 

auto delInvmt = []( Investment* pInvestment)

{ //自定义的

makeLogEntry(pInvestment); //deleter

delete pInvestment; //(一个lambda表达式)

};

template<typename... Ts>

std::unique_str<Investment,decltype(delInvmt)>

makeInvestment(Ts&&... params)

{

std::unique_ptr<Investment,decltype(delInvmt)> pInv(nullptr,delInvmt);

if(/*一个Stock对象需要被创建*/)

{

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

}

else if(/*一个Bond对象应该被创建*/)

{

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

}

else if(/*一个RealEstate对象应该被创建*/)

{

pInv.reset(new RealEstate(std::forward<Ts>(params)...));

}

return pInv;

}

 

马上我会解释这是怎么工作的,但是首先考虑如果你是调用者事情看起来是怎么样的。假设你将调用makeInvestment返回的结果存储在一个auto变量里,你是活在幸福中的,因为你不需要知道你使用的资源在销毁时需要特殊对待。事实上,你真的是沐浴在幸福中,因为std::unique_ptr的使用意味着你不需要关心它是怎么销毁的,更不确保程序的每一条执行路径中,资源都确实能进行销毁。std::unique_ptr自动把这些事做了。从一个客户的角度来说,makeInvestment的接口是良好的。

一旦你理解了下面的东西,你会发现它的实现也是非常好的:

1.delInvmtmakeInvestment函数返回对象的自定义deleter。所有的自定义销毁函数接受一个原生指针(指向要被销毁的对象),然后做一些要销毁这个对象必要的操作。在本例中,调用了makeLogEntry函数,然后应用了delete。使用lambda表达式创建delInvmt是很方便的,但是,就像我们将要看到的那样,这比写一个传统的函数要有效率的多。

2.当一个自定义deleter被应用时,它的类型必须被指定为std::unique_ptr的第二个类型参数。在本例中,就是delInvmtde类型,这就是为什么makeInvsetment的返回类型是std::unique_ptr<Investment,decltype(delInvmt)>(关于decltype的类型查看Item3)。

3.makeInvestment的一个基本策略是创建一个空std::unique_ptr,让它指向一个合适类型的对象,然后返回它。为了将自定义deleterpInv联系起来,我们将它作为构造函数的第二个参数传入。

4.企图将一个原生指针(例如,new出来的)赋值给std::unique_ptr是不通过编译的,因为它会造成一个从原生指针向智能指针的转换。这样的转换是有问题的,所以c++11的智能指针禁止了它们。这就是为什么reset被用来获得new出来对象的所有权。

5.每一次对new的调用,我们用std::forward去完美转发传入makeInvestment的参数。这会让调用者提供的所有信息被利用到被创建的对象的构造函数中。

6.自定义deleter接受一个Investment*类型的参数。不管makeInvestment里创建的对象的真实类型是什么(例如StockBond或者RealEstate),它最终在lambda表达式中作为Investment*的对象呗delete掉。这意味着我们通过基类指针delete派生类对象。因此,基类Investment必须具有一个虚拟析构函数:

 

class Investment {

public:

...

virtual ~Investment();

...

}

 

c++14中,函数返回类型推断的存在意味着makeInvsetment能被实现成更优雅,封装性更好的方式:

 

template<typename... Ts>

auto makeInvestment(Ts&&... params)

{

std::unique_ptr<Investment,decltype(delInvmt)> pInv(nullptr,delInvmt);

if(/*一个Stock对象需要被创建*/)

{

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

}

else if(/*一个Bond对象应该被创建*/)

{

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

}

else if(/*一个RealEstate对象应该被创建*/)

{

pInv.reset(new RealEstate(std::forward<Ts>(params)...));

}

return pInv;

}

 

我之前说过,当使用默认deleter时,你有理由假定std::unique_ptr对象有着和原生指针一样的大小。当自定义deleter参合进来后,情况也许就不是这样了。当deleter是函数指针的时候,一般会造成std::unique_ptr从一个字节涨到两个字节。当deleter是仿函数时,变化的大小依赖于仿函数中存储的状态有多少。没有状态的仿函数(比如,不捕获变量的lambda表达式)遭受的大小的惩罚是0(不会改变大小),这意味着当自定义deleter能被实现为函数或lambda表达式时,lambda是更好的选择:

 

auto delInvmt1 = [](Investment* pInvestment)

{

makeLogEntry(pInvestment);

delete pInvestment;

};

template<typename... Ts>

std::unique_ptr<Investment, decltype(delInvmt1)>

makeInvestment(Ts&&... args);

 

void delInvmt2(Investment* pInvestment)

{

makeLogEntry(pInvestment);

delete pInvestment;

};  

template<typename... Ts>

std::unique_ptr<Investment, void(*)(Investment*)>

makeInvestment(Ts&&... args);

 

带大量状态的仿函数会产生大小很大的std::unique_ptr。如果你发现一个自定义deleter让你的std::unique_ptr变得不可接受的大,你也许要改变你的设计。

工厂函数不是std::unique_ptr唯一的惯用情况。它在实现Pimpl机制时更加受欢迎。这样的代码不是很复杂,但是也不是直截了当的,所以我会在Item 22中提及,那个Item是致力于这个话题的。

std::unique_ptr有两种形式,一种是单个对象的(std::unique_ptr<T>)一种是数组的(std::unique_ptr<T[]>)。因此,这里永远不会有任何模糊的情况:对于std::unique_ptr指向的是数组还是单独的对象。std::unique_ptrAPI的设计符合你的使用习惯。举个例子,单个对象没有下标操作,同时数组的形式没有解引用操作(operator*operator->)。

std::unique_ptr数组形式的存在对你来说应该仅仅作为一项有趣的技术,因为std::array,std::vector,std::string都比原始数组是更好的数据结构的选择。关于我能想象到的唯一的情景使得std::unique_ptr是有意义的,那就只有当你使用类CAPI时(并且它返回一个原始指针,指向堆上的数组,同时你拥有它的所有权)。

std::unique_ptrc++11表达独自占有权的方法,但是它最吸引人的特性之一是它可以很容易且很有效率的转换为std::shared:

 

std::shared_ptr<Investment> sp =

makeInvestment( arguments );

 

这是std::unique_ptr如此适用于工厂函数返回类型的关键点。工厂函数并不知道调用者想要返回对象资源的独占权还是共享权。通过返回一个std::unique_ptr,工厂提供给调用者最高效的智能指针,但是他不阻碍调用者将其转换为更灵活的兄弟(std::shared_ptr)。

 

Things to Remember

1.std::unique_ptr是一个小的,快的,mov-only的智能指针,它能用来管理资源,并且独占资源的所有权。

2.默认情况下,资源的销毁是用过delete进行的,但是自定义deleter能指定销毁的行为。用带状态的deleter和函数指针作为deleter会增加std::unique_ptr对象的大小。

3.std::unique_ptr转换到std::shared_ptr很简单。