条款18: 使用std::unique_ptr来进行独享所有权的资源管理
当你接触到智能指针,那么std::unique_ptr应该是最常见到的。通常我们可以认为std::unique_ptr和原始指针有一样的尺寸,并且对于多数操作(包括解引用),它和原始指针有几乎相同的CPU指令。这意味着你可以在内存和CPU紧张的地方使用它,如果普通的原始指针对你够快够用的话,那么std::unique_ptr几乎也是够用的。
std::unique_ptr实现了独享所有权的语义。一个非空的std::unique_ptr总是拥有它所指向的资源。转移一个std::unique_ptr将会把所有权也从源指针转移给目标指针(源指针被置空)。拷贝一个std::unique_ptr将不被允许,因为如果你拷贝一个std::unique_ptr,那么拷贝结束后,这两个std::unique_ptr都会指向相同的资源,它们都认为自己拥有这块资源(所以都会企图释放)。因此std::unique_ptr是一个仅能移动(move_only)的类型。当指针析构时,它所拥有的资源也被销毁。默认情况下,资源的析构是伴随着调用std::unique_ptr内部的原始指针的delete操作的。
std::unique_ptr一个常见的使用是在一个工厂函数中,其返回类型是一个对象,且包含了继承关系。假如我们有一个投资(investments)继承关系(股票,分红,不动产):
class Investment { … };
class Stock:
public Investment { … };
class Bond:
public Investment { … };
class RealEstate:
public Investment { … };
一个典型的工厂方法(factory function)会分配堆内存,返回指针,调用者负责在不需要时释放内存。这正好是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数据成员也会被销毁,这会导致资源也被释放。假如这个转移链因为异常或者其他不确定的控制流(比如提前的函数返回或者从循环中break)而中断,那么std::unique_ptr最终也会调用其管理的资源的析构,资源因此还是会被释放。
通常,释放会通过调用delete来触发,但是在构建过程中,std::unique_ptr对象可以被指定特定的释放者:任意的函数(或者函数对象,包括lamda表达式),当需要释放资源时,这些函数被呼起(call)。假如一个由makeInvestment构造的对象不直接被删除,而是应该在入口处打印log,那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的接口很甜美。
实现也很优美,只要你一旦了解下面的描述:
1. delInvmt是为 makeInvestment返回对象定制的删除器,所有的删除定制函数接受一个待删除的原始指针,然后删除之。在这里,动作是先调用makeLogEntry,然后调用delete。使用lamda表达式来创建delInvmt很方便,也比写一个普通函数更高效。
2.当使用定制的删除器时,其类型必须作为std::unique_ptr的第二个参数。在这里,就是delInvmt的类型,这就是为什么makeInvestment的返回类型是std::unique_ptr<Investment, decltype(delInvmt)>(更多信息参见delctype,条款3).
3.makeInvestment的基本策略是创建一个空的std::unique_ptr,使它指向一个合适类型的对象,然后返回。为了关联定制删除器和pInv,我们把它作为第二个参数传给构造函器。
4.企图赋值一个原始指针(比如从new来的)给std::unique_ptr是不能编译通过的,因为它引起了一个从原始指针到智能指针的隐式转换。这个隐式转换会出问题的,所以c++11的智能指针禁止这个转换。这就是为什么reset被用来确定pInv拥有从new得到的指针的所有权。
5.对每一个new,我们使用std::forward来完美的传递参数给makeInvestment (见条款25)。这个会使得所有调用者提供的信息都能适用于创建这个对象的构造函数。
6.定制删除器接收一个类型为Investment*的参数,不管在makeInvestment内部实际创建的对象类型是什么(比如Stock, Bond, 或RealEstate),终究会在lamda表达式中当成一个Investment*对象来被删除。这意味着我们会通过一个基类指针来删除一个派生类对象。为此,基类--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增加到两个word。使用函数对象的删除器,尺寸变化的大小取决于函数对象中存储的状态多少。无状态的函数对象(比如无捕获的lamda表达式)没有尺寸的增长。这意味着当一个定制删除器既可以用函数实现,又可以用无捕获的lamda表达式来实现时,lamda会更好一些。
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对象大小增加。因此假如你发现一个定制删除器使得你的std::unique_ptr变得不可接受的增大时,你可能最好需要改变一下设计了。
工厂函数不仅通常用在std::unique_ptr,也更加用来实现防火墙机制。其代码不复杂,但有时也不是那么直接,所以我推荐你看条款22,专门描述了这个主题。
std::unique_ptr通常出现两种形式。一种是单个对象(std::unique_ptr<T>),另一种是数组(std::unique_ptr<T[]>)。这两者对于它们指向的对象绝对没有混淆。std::unique_ptr的API被设计成匹配你使用的形式。比如单个对象形式没有索引操作符(operator[]),而数组形式也没有解引用操作(operator* 和operator->)。
关于std::unique_ptr的数组形式可能仅仅有一些智力上的意义,因为std::array,std::vector,std::string通常是比原始指针更加好的数据结构,我能想到的唯一使用std::unique_ptr<T[]>的情况是你正在使用一个C-like的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请看条款19)。
需要记住的事情:
1.std::unique_ptr是一个小巧,迅速,仅能移动(move-only)的灵巧指针,它通过独享所有权的语义来管理资源。
2.默认情况下,资源释放是通过delete来析构,但是定制删除器可以被用来指定。有状态的删除器和函数指针删除器会增加std::unique_ptr对象的尺寸大小。
3.std::unique_ptr很容易转换成std::shared_ptr。