Item 18: Use std::unique_ptr for exclusive-ownership resource management

当你使用针时,std::unique_ptr通常应该是最被先想到的。可以认为在默认情况下,std::unique_ptr和裸指针具有同样的大小,并且对于大多数操作(包括解引用),它们执行的是完全相同的指令。这意味着在内存和cpu紧张的情况下你也可以用它。如果一个原始指针对你来说足够的小和快,那么一个std::unique_ptr也几乎是这样的。

std::unique_ptr表现出独占所有权的语义。一个非空的std::unique_ptr总是拥有其所指向的资源。move一个std::unique_ptr将把所有权从源指针转交给目标指针(源指针将被设置为null)。copy一个std::unique_ptr是被禁止的,因为如果你拷贝一个std::unique_ptr,你将得到两个指向同样资源的std::unique_ptr,这两个指针都认为自己独占该资源(也都认为应该负责释放资源)。因此std::unique_ptr是一个move-only(只能进行move操作的)类型。在执行析构时,由非空的std::unique_ptr的析构其资源。默认情况下,通过对std::unique_ptr内的原始指针应用delete方法来进行资源销毁的。

std::unique_ptr的常见用途是作为对象继承结构中作为工厂函数的返回类型。假设我们有一个投资类型的类层次(比如,股票,债券,不动产等等),的基类是Investment。

class Investment {};

class Stock : public Investment {};

class Bond : public Investment {};

class RealEstate : public Investment {};

这种层次结构的工厂函数通常在堆上分配一个对象,并返回一个指向该对象的指针,调用者负责在不再需要对象时删除该对象。这完全符合std::unique_ptr的理念,因为调用者要对工厂返回的资源负责(也就是,它独占了所有权),当std::unique_ptr被销毁的时候,std::unique_ptr会自动销毁它指向的对象。Investment继承层次中的工厂函数应该被声明成这样:

template<typename... Ts>                // return std::unique_ptr
std::unique_ptr<Investment>             // to an object created
    makeInvestment(Ts&&... params);     // from the given args

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

{
    ...
    auto pInvestment =                  // pInvestment is of type
        makeInvestment( arguments );    // std::unique_ptr<Investment>
    ...
}                                       // destroy *pInvestment

但是,它们也可以在所有权转移的场景中使用它,例如,当从工厂返回的std::unique_ptr被移动到容器中时,容器元素随后又被移动到对象的数据成员中,并且该对象随后被销毁。这时,该对象的std::unique_ptr数据成员也会被销毁,它的销毁将导致从工厂返回的资源也被销毁。如果由于一个异常或者其他的非正常的控制流(比如,在循环中return或break ),所有权链被中断了,持有被管理资源的std::unique_ptr最终还是会调用它的析构函数,被管理的资源还是会被销毁。

默认地,销毁是通过delete操作符实现的,但是在构造std::unique_ptr时,可以给它配置自定义的删除器:任意的自定义函数(或仿函数,包括通过lambda表达式产生的仿函数)将在资源销毁的时候被调用。如果makeInvestment创建的对象不应该被直接delete,而是应该先写入一条日志,那么makeInvestment的实现可以像下面这样(代码后面跟着注释,所以如果你看到一些不明确的代码,不需要担心):

auto delInvmt = [](Investment* pInvestment)        // custom
				{                                  // deleter
					makeLogEntry(pInvestment);     // (a lambda
					delete pInvestment;            // expression)
				};

template<typename... Ts>                           // revised
std::unique_ptr<Investment, decltype(delInvmt)>    // return type
makeInvestment(Ts&&... params)
{
	std::unique_ptr<Investment, decltype(delInvmt)>  // ptr to be
		pInv(nullptr, delInvmt);                     // returned

	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 object should be created */ )
	{
		pInv.reset(new RealEstate(std::forward<Ts>(params)...));
	}

	return pInv;
}

我马上会解释这段代码是怎么工作的,但是现在,我们先考虑下如果你是一个调用者,事情看起来会是怎样。假设你将makeInvestment的调用结果存储在auto变量中,你欣然地忽略正在使用的资源需要在析构时加以特殊处理这一事实。事实上,你真的应该喜出望外,因为std::unique_ptr的使用,你无需要担心资源何时被销毁,更不用说确保在程序的每条路径上精确地销毁一次了。Std::unique_ptr会自动处理所有这些事情。从客户的角度看,makeInvestment的接口太棒。了。

一旦理解了以下内容,你就会发现这段代码不仅接口很棒,实现也很不错:

  • delInvmt是makeInvestment返回的对象(std::unique_ptr对象)的自定义deleter,所有自定义删除函数都接受一个指向要销毁对象的原始指针,然后做一些在销毁对象时必须做的事。本例中所采取的措施就是调用makeLogEntry然后再delete。使用lambda表达式创建delInvmt很方便,正如我们很快将看到的,它也比编写常规函数更高效。
  • 当使用自定义删除器时,它的类型必须被指定为std::unique_ptr的第二个类型参数。本例中,就是delInvmt的类型,这就是为什么makeInvestment的返回类型是std::unique_ptr<Invest, decltype(delInvmt)>。(关于decltype的信息,请参见条款3)
  • makeInvestment的基本策略是创建一个空的std::unique_ptr,使其指向适当类型的对象,然后返回它。为了将自定义删除器delInvmt与pInv关联起来,我们将delInvmt作为它的第二个构造实参。
  • 尝试把一个原始指针(比如,从new返回的)赋值给一个std::unique_ptr是无法通过编译的,因为这被视为从原始指针到智能指针的隐式转换,这种隐式转换是有问题的,所以C++11的智能指针禁止这种操作。这也就是为什么需要使用reset来指定让pInv获取从new创建对象的所有权;
  • 每次使用new,我们都会用std::forward对传递给makeInvestment的实参实施完美转发(见条款25)。这可以使得所创建对象的构造函数能够获得调用者提供的所有信息;
  • 自定义删除器接受一个类型为Investment*的参数。不管在makeInvestment中创建的对象的实际类型是什么(股票、债券或不动产)
    属性),它将最终会在lambda表达式中作为一个Invest *对象被删除。这意味着我们将通过基类指针删除派生类对象。为此,基类investment必须有一个虚析构函数:
class Investment {
public:// essential
    virtual ~Investment();        // design// component!
};

在c++ 14中,函数返回类型推导(见条款3)的存在意味着makeInvestment可以以这种更简单、更封装的方式实现:

template<typename... Ts>
auto makeInvestment(Ts&&... params) // C++14
{
    auto delInvmt = [](Investment* pInvestment)    // this is now
                    {                              // inside
                        makeLogEntry(pInvestment); // make-
                        delete pInvestment;        // Investment
                    };
std::unique_ptr<Investment, decltype(delInvmt)>    // as
    pInv(nullptr, delInvmt);                       // before
    
    if ()                                       // as before
    {
        pInv.reset(new Stock(std::forward<Ts>(params)...));
    }
    else if ()                                  // as before
    {
        pInv.reset(new Bond(std::forward<Ts>(params)...));
    }
    else if ()                                  // as before
    {
        pInv.reset(new RealEstate(std::forward<Ts>(params)...));
    }
    return pInv;                                   // as before
}

我之前提到过,在使用默认删除器时(即delete运算符)时,你可以认为std::unique_ptr的大小和裸指针一样。但当自定义删除器出现时,情况有所不同了。如果删除器是函数指针,则std::unique_ptr的大小会从一个字(word)增加到两个。对于函数对象的删除器,大小的变化取决于函数对象中存储了多少状态。无状态函数对象(例如,来自没有捕获的lambda表达式)不会浪费任何存储尺寸,这意味着当自定义删除器既可以应用函数实现,也可以应用无捕获的lambda表达式实现时,lambda是更好的选择:

auto delInvmt1 = [](Investment* pInvestment)      // custom
                {                                 // deleter
                    makeLogEntry(pInvestment);    // as
                    delete pInvestment;           // stateless
                };                                // lambda
template<typename... Ts>                          // return type
std::unique_ptr<Investment, decltype(delInvmt1)>  // has size of
makeInvestment(Ts&&... args);                     // Investment*

void delInvmt2(Investment* pInvestment)           // custom
{                                                 // deleter
     makeLogEntry(pInvestment);                   // as function
    delete pInvestment; 
} 
  
template<typename... Ts>                          // return type has
std::unique_ptr<Investment,                       // size of Investment* 
                void (*)(Investment*)>            // plus at least size 
makeInvestment(Ts&&... params);                   // of function pointer!

删除器采用带有大量状态的函数对象实现,可能使得std::unique_ptr对象尺寸增加的较多。如果你发现一个自定义deleter让你的std::unique_ptr大到无法接受,你可能需要改变你的设计了。

工厂函数不是std::unique_ptr唯一的使用情况。它们在实现Pimpl机制的时候更加受欢迎。这样的代码不是很复杂,但也不是十分直截了当,所以推荐阅读条款22,它专门讨论了这个主题。

std::unique_ptr有两种形式,一种是给单个对象(std::unique_ptr)用的,另一种是给数组(std::unique_ptr<T[]>)用的。这样区分的结果是,对于std::unique_ptr所指向的对象种类不会产生二义性。std::unique_ptr的API也被设计成与使用形势相匹配的。比如,单个对象的形式不提供索引操作(operator[]),而数组形式不提供解引用操作(operator* and operator->)。

std::unique_ptr数组形式的存在,作为一种知识的了解就够了,因为std::array, std::vector, and std::string几乎总是比原始数组更好用。唯一一个我能想到std::unique_ptr<T[]>的合理使用场景是使用了一个C风格的API,它返回一个指向数组的原始指针。

std::unique_ptr是在C++11中表达独占所有权的方式,但是它还有一个吸引人的特性,就是它可以方便并高效地转换成std::shared_ptr:

std::shared_ptr<Investment> sp =        // converts std::unique_ptr
    makeInvestment( arguments );        // to std::shared_ptr

这就是为什么std::unique_ptr这么适合作为工厂函数的返回值类型的关键所在。工厂函数不知道调用者是否想要把对象用在独占所有权的语义上还是共享所有权(也就是std::shared_ptr)的语义上。通过返回一个std::unique_ptr,工厂提供给调用者一个最高效的智能指针,但是他们不阻止调用者把它转换成更具灵活的其他兄弟(std::shared_ptr)。(关于std::shared_ptr,参考Item 19)

Things to Remember

  • std::unique_ptr是一个小巧、快速、只可move的智能指针,它用在管理独占所有权资源的语义中;
  • 默认地,资源销毁通过delete来实现,但可以指定自定义删除器。有状态的删除器和函数指针会增加std::unique_ptr的大小;
  • 可以轻松的将std::unique_ptr转换成std::shared_ptr;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值