18.在进行独占式资源管理时使用std::unique_ptr

前言

 
unique_ptr的大小与原始指针相同(在大多数情况),操作起来也类似,这意味着你可以某些内存吃紧与实时性较高的场合(例如嵌入式?)中使用它们。


unique_ptr定义

 
std::unique_ptr代表着独占语义。任何一个非空unique_ptr总是唯一拥有其指向资源。对unique_ptr执行移动操作将导致资源所有权从源指针传递到目标指针(源指针被置为null),unique_ptr不允许copy操作,因为这会导致存在两个指针指向同一份资源,与独占性相违。由此可见,unique_ptr是一种move_only type,在它析构时,默认将对其所拥有的原始指针执行delete操作。

问题实例

 
一般来说,unique_ptr常用于作为工厂函数的返回类型(当然,工厂函数并非std::unique_ptrs的唯一常见用例,它们也经常作为Pimpl手法的实现机制而得到使用,具体可见Item22)。
假设当前有一个代表投资的继承体系如下所示:
image_1chigac1e1b531m0r1cs5o81bmj9.png-17.6kB

1
2
3
4
class Investment { … };
class Stock:public Investment { … };
class Bond:public Investment { … };
class RealEstate:public Investment { … };

工厂函数将在堆上生成一个对象并返回一个指向它的指针,调用者负责在不再需要时删除该对象。这正是std::unique_ptr的职责所在:

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

调用者可在某作用域内使用工厂函数返回值如下所示:
1
2
3
4
5
{

auto pInvestment = makeInvestment(arguments); // std::unique_ptr<Investment>

}// destroy *pInvestment

但也可以在所有权迁移场景中使用unique_ptr,例如将工厂函数的返回值置入容器,而容器元素又在其后成为了某个对象的data member,其后该对象被销毁。当发生这种情况时,对象的std::unique data member也将被销毁,并且其销毁将导致工厂函数所返回的资源被释放。如果在上述过程中出现了异常或逻辑错误(函数过早返回或循环由于break退出),那么管资源的std::unique_ptr最终将调用其析构函数,释放其托管的资源。

在默认情况下,该资源释放将通过使用delete完成,但在构造过程中,std::unique_ptr对象可以配置自定义删除器:任意函数、函数对象、lambda等等。在unqie_ptr析构之时,这些删除器将负责完成资源的释放操作。举例而言,如果makeInvestment所创建的对象不应该直接删除,而应首先完成日志记录操作,makeInvestment可实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
auto delInvmt = [](Investment* pInvestment) {makeLogEntry(pInvestment);delete pInvestment;};//lambda
template<typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt)>
makeInvestment(Ts&&... params){
std::unique_ptr<Investment, decltype(delInvmt)> 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 object should be created */ ){
pInv.reset(new RealEstate(std::forward<Ts>(params)...));
}
return pInv;
}


实现剖析

  1. delInvmt是工厂函数返回对象的自定义删除器,其接受指向应被销毁的对象的原始指针,然后执行销毁该对象所需的操作。 在本例中,销毁操作时调用makeLogEntry然后应用delete。使用lambda表达式创建delInvmt十分便捷,但实际上它并不止便捷那么简单。
  2. 当需要使用自定义删除器时,必须将其类型指定为std::unique_ptr的第二个模板类型参数。因此在本例中makeInvestment的返回类型是std::unique_ptr<Investment,decltype(delInvmt)>。
  3. 将原始指针(例如从new得到的资源)对std::unique_ptr进行赋值将无法编译,因为这一行为构成了从原始指针到智能指针的隐式转换,C++11禁止这种隐式转换,因此使用reset成员函数。
  4. 每次调用new时,我们都使用std::forward来实现完美转发makeInvestment获得的参数(见Item25)。这一行为使得调用者提供的所有信息都可被正在创建的对象的constructor所采纳。
  5. 为了保证删除器能够正确完成析构操作,基类必须具备virtual析构函数(因为我们是通过一个指向基类的指针来执行delete):
    1
    2
    3
    4
    5
    6
    class Investment {
    public:
    // essential
    virtual ~Investment(); // design
    // component!
    };

C++14提供的工厂函数实现

 
C++14支持返回值类型推衍,因此C++14中工厂函数的实现可如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename... Ts>
auto makeInvestment(Ts&&... params){
auto delInvmt = [](Investment* pInvestment){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;
}


自定义删除器对unique_ptr的影响

 
前文曾经提及,当使用默认删除器(即delete)时,可以合理地假设std::unique_ptr对象与原始指针的大小相同。但一旦存在自定义删除器,那么该假设一般不再成立。如果删除器是函数指针类型,含有自定义删除器的std::unique_ptr大小往往从1个word增长至2个word。如果删除器是函数对象,那么unique_ptr增长的大小视函数中状态的多少而定。无状态函数对象(例如没有捕获的lambda表达式)不会导致unique_ptr的大小发生变化,这意味着当自定义删除器可实现为函数或无捕获的lambda表达式时,lambda更为可取:

1
2
3
4
5
6
7
8
9
10
11
auto delInvmt1 = [](Investment* pInvestment){makeLogEntry(pInvestment);delete pInvestment;}; 
template<typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt1)>
makeInvestment(Ts&&... args);
void delInvmt2(Investment* pInvestment){// function object
makeLogEntry(pInvestment);
delete pInvestment;
}
template<typename... Ts>
std::unique_ptr<Investment,void (*)(Investment*)>
makeInvestment(Ts&&... params);

具有状态的函数对象删除器可能会生产较大的unique_ptr对象。如果你发现自定义删除器std::unique_ptrs大到了不可接受的程度,则可以考虑更改删除器类型。


unique_ptr与数组

 
std::unique_ptr存在两种形式,一种用于单个对象(std::unique_ptr<T>),另一种用于数组(std::unique_ptr<T[]>)。因此,对于std::unique_ptr指向的实体类型从不存在任何歧义。std::unique_ptr的API视当前使用情况而定。例如,指向单一对象时unique_ptr没有operator[],而指向数组时则缺少operator*和operator->。

但指向数组的unique_ptr并无必要,因为STL中的容器比数组好用太多,std::unique_ptr<T[]>唯一有存在价值的情况就是当你使用类C的API时,它会返回一个原始指针,指向你独占的堆数组。


unique_ptr与shared——ptr

 
std::unique_ptr最吸引人的特性之一是它可以轻松高效隐式地转换为std::shared_ptr:

1
std::shared_ptr<Investment> sp = makeInvestment( arguments );

这即是std::unique_ptr适合作为工厂函数返回类型的关键原因所在。工厂函数无法知道调用者是否希望对返回的对象使用独占语义,或者共享语义。通过返回std::unique_pt,工厂函数得以保证当调用者需要实现共享语义时可以轻松从unique_ptr转换得到shared_ptr。


总结

  1. std::unique_ptr是一种小型、快速、move-only的智能指针,其在管理资源时具备独占语义。
  2. 在默认情况下,资源释放通过delete完成,但用户亦可自定义删除器。存有状态的删除器和函数指针会增加std::unique_ptr对象的大小。
  3. 将std::unique_ptr转换为std::shared_ptr十分容易。