使用std::unique_ptr管理具备专属所有权的资源
每当你需要使用智能指针的时候,std::unique_ptr
基本是最合适的。可以认为在默认情况下,std::unique_ptr
等同于原始指针,而且对于大多数操作(包括解引用),它们执行的指令完全相同。这意味着你甚至可以在内存和时间都比较紧张的情况下使用它。如果原始指针够小够快,那么std::unique_ptr
几乎可以肯定也能满足你的要求。
std::unique_ptr
体现了专有所有权语义。一个非空的std::unique_ptr
总是拥有其所指向的资源。移动一个std::unique_ptr
会将所有权从源指针移动到目标指针(原指针被置空)。拷贝操作是不允许的,因为如果你能拷贝一个std::unique_ptr
,你会得到指向相同内容的两个std::unique_ptr
,每个都认为自己拥有资源,销毁时也会出现重复销毁。因此,std::unique_ptr
只支持移动操作。当std::unique_ptr
销毁时,其指向的资源也执行析构函数。而原始指针需要显式调用delete
来销毁指针指向的资源。
std::unique_ptr
的常见用法是作为继承层次结构中对象的工厂函数返回类型。假设我们有一个基类Investmen
的继承结构。
class Investment{ ... };
class Sock : public Investment { ... };
clsas Bond : public Investment { ... };
class RealEstate : public Investment { ... };
这种继承关系的工厂函数在堆上分配一个对象然后返回指针,调用方在不需要的时候,销毁对象,这使用场景完美匹配std::unique_ptr
,因为调用者对工厂返回的资源负责(即堆该资源的专有所有权),并且std::unique_ptr
会自动销毁指向的内容。可以这样声明
template<typename... Ts>
std::unique_ptr<Investment>
makeInvestment(Ts&&... params);
调用者应该在单独的作用域中使用返回的std::unique_ptr
智能指针:
{
...
auto pInvestment = makeInvestment(arguments);
...
} //destroy *pInvestment
如果所有权链由于异常或者其他非典型控制流出现中断(比如提前return
或者循环中的break
),则拥有托管资源的std::unique_ptr
将保证指向的内容的析构函数被调用,销毁对应资源。
默认情况下,销毁将通过delete
进行,但是在构造过程中,可以自定义std::unique_ptr
指向对象的析构函数:任意函数(或者函数对象,包括lambda
)。如果通过makeInvestment
创建的对象不能直接被删除,应该首先写一条日志,可以实现如下:
auto delInvmt = [](Investment* pInvestment)
{
makeLogEntry(pInvestment);
delete pInvestment;
}
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;
}
这个实现确实相当棒,如果你理解了:
delInvmt
是自定义的从makeInvestment
返回的析构函数。所有的自定义的析构行为接受要销毁对象的原始指针,然后执行销毁操作。- 当使用自定义删除器时,必须将其作为第二个参数传给
std::unique_ptr
。 makeInvestment
的基本策略时创建一个空的std::unique_ptr
,然后指向一个合适的类型的对象,然后返回,为了为pInv
关联自定义删除器,作为构造函数的第二个参数。- 尝试将原始指针(比如
new
创建)赋值给std::unique_ptr
通不过编译,因为不存在从原始指针到智能指针的隐式转换,这种隐式转换为出问题,所以禁止,这就是为什么通过reset
来传递new
指针的原因。 - 自定义删除器的参数类型是
Investment*
,尽管真实的对象类型是在makeInvestment
内部创建的,它最终通过在lambda
表达式中,作为Investment*
对象被删除。这意味着我们通过基类指针删除派生类实例。为此,基类必须是虚析构函数。 - 使用
new
时,要使用std::forward
作为参数来完美转发给makeInvestment
,这使调用者提供的所有信息可用于正在创建的对象的构造函数。
class Investment{
public:
...
virtual ~Investment();
...
};
当使用默认删除器时,可以合理假设std::unique_ptr
和原始指针大小相同。当自定义删除器时,情况可能不再如此。删除器是个函数指针,通常会使std::unique_ptr
的字节从一个增加到两个。对于删除器的函数对象来说,大小取决于函数对象中存储的状态多少,无状态函数对象(比如没有捕获的lambda
表达式)对大小没有影响,这意味当自定义删除器可以被lambda
实现时,尽量使用lambda
。
auto delInvmt = [](Investment* pInvestment)
{
makeLogEntry(pInvestment);
delete pInvestment;
};
template<typename... Ts>
std::unique_ptr<Investment,decltype(delInvmt)>
makeInvestment(Ts&& params); //返回值尺寸与Investment*相同
void delInvmt2(Investment* pInvestment)
{
makeLogEntry(pInvestment);
delete pInvestment;
}
template<typename... Ts>
std::unique_ptr<Investment, void(*)(Investment*)>
makeInvestment(Ts&&... params); //返回值尺寸等于Investment*的尺寸加上至少函数指针的尺寸
std::unique_ptr
有两种形式,一种用于单个对象(std::unique_ptr<T>
),一种用于数组(std::unique_ptr<T[]>
)。
数组的std::unique_ptr
的存在应该不被使用,因为std::array
,std::vector
,std::string
这些更好用的数据容器应该取代原始数组。
std::unique_ptr
是C++11中表示专有所有权的方法,但是其最吸引人的功能之一是它可以轻松高效的转换为std::shared_ptr
std::shared_ptr<Investment> sp = makeInvestment(arguments);
这就是为什么std::unique_ptr
非常适合用于工厂函数返回类型的关键部分。工厂函数无法直到调用者是否要对它们返回的对象使用专有所有权的语义,或者共享所有权std::shared_ptr
是否更合适。通过返回std::unique_ptr
,工厂为调用者提供了最有效的智能指针,但它们并不妨碍调用者用更灵活的兄弟替换它。
要点速记
std::unique_ptr
是轻量级,快速的,具备只移类型的智能指针,对托管资源实施专属所有权定义- 默认的,资源析构采用
delete
运算符实现,但可以指定自定义删除器。有状态的删除器和采用函数指针实现的删除器会增加std::unique_ptr
类型的对象尺寸 - 将
std::unique_ptr
转换成std::shared_ptr
容易实现。