std::unique_ptr
占用空间与裸指针相同,大多数操作(包括解引用)的方法也相同。它表现的是 独占性拥有(exclusive ownership) 的语义,这意味着它不能被拷贝,是 move-only 类型;当其自身被销毁时,总会销毁其拥有的资源。而对象的销毁在作用域结束(即右大括号出现)时自动通过调用析构函数进行,此时std::unique_ptr
默认对内部的裸指针进行delete
,因此你无需像裸指针一样操心什么时候要手动销毁。- (注:笔者尽己所能对MSVC的
std::unique_ptr
源码进行了一些解析,为了不影响整体观感,放在全文最后) std::unique_ptr
可以作为工厂函数的返回类型。假设有一个基类 Investment(投资)以及若干派生类 Stock(股票),Bond(债券),RealEstate(不动产)。工厂函数makeInvestment
根据参数params
生成对象,并以std::unique_ptr<Investment>
类型返回:
template<typename... Ts>
std::unique_ptr<Investment>
makeInvestment(Ts&&... params);
std::unique_ptr
的析构行为默认是delete
,可以在构造时传入自定义的删除器(deleter):一个任意的函数(或仿函数对象,包括 lambda 表达式)。
template<typename... Ts>
auto makeInvestment(Ts&&... params) // C++14,自动推导函数返回值类型
{
auto delInvmnt = [](Investment* pInvestment)
{
makeLogEntry(pInvestment); // 析构前先做某种日志记录
delete pInvestment; // 任何派生类都会通过调用Investment的析构函数销毁
// 因此Investment的析构函数必须是virtual!
};
std::unique_ptr<Investment, decltype(delInvmnt)> pInv(nullptr, delInvmet);
if ( /* 应该创建Stock对象 */)
{
pInv.reset(new Stock()); // unique_ptr通过reset()函数重新设置管理的资源
}
else if ( /* 应该创建Bond对象 */)
{
pInv.reset(new Bond());
}
else if ( /* 应该创建RealEstate对象 */)
{
pInv.reset(new RealEstate());
}
return pInv;
}
-
使用默认删除时,
std::unique_ptr
和裸指针占用相同的大小。需要自定义 deleter时,尽可能使用 lambda 表达式而非普通函数,使用前者不会带来占用空间的增加,而使用后者会多占用至少一个字长的空间!
-
std::unique_ptr
在 Pimpl 范式中的应用更加流行,详见Item 22。 -
std::unique_ptr
有两个版本,一个针对单独对象std::unique_ptr<T>
,一个针对数组std::unique_ptr<T[]>
。二者之间区分明确,例如前者没有下标运算符(operator []
),而后者没有解引用运算符(operator *
和operator ->
)。(笔者注:查看源码可以发现,二者实际是两个分开定义的类)不过后者的应用场景很少,主要可以用于与返回数组指针的C风格API对接。 -
std::unique_ptr
的一个出色特性是可以轻易且高效地转成std::shared_ptr
。由此,工厂函数无需知道调用者是否一定会将其独占使用——他们只需返回最高效的智能指针,至于调用者是将其作为std::unique_ptr
还是std::shared_ptr
使用都与他们无关。对于调用者而言的使用体验也是很愉快的。
// converts std::unique_ptr to std::shared_ptr
std::shared_ptr<Investment> sp = makeInvestment( arguments );
总结
std::unique_ptr
是一个小巧、高效、move-only 的智能指针,用于管理独占性资源。- 资源的销毁默认通过
delete
进行,但是可以声明自定义的 deleter。有状态的 deleter 和函数指针作为 deleter 会增加std::unique_ptr
对象的大小。 - 将
std::unique_ptr
转换为std::shared_ptr
是很轻松的。
附:对MSVC的STL中 std::unique_ptr
实现的一些分析
此部分网络上基本没有参考资料,主要基于笔者阅读源码的推测,可能有错误,欢迎各位大神指出。
- 首先如前文所述,
std::unique_ptr
分单个对象和数组两个版本,相应的也有两个make_unique
版本函数。以下主要针对单对象版本说明。模板参数将数据类型和 deleter 类型分别称为_Ty
和_Dx
。 - 从声明中可以看出,默认的 deleter 使用了一个结构体对象
default_delete
,它是重载了operator ()
的仿函数。其行为自然是delete
掉一个_Ty
类型的指针。
- 回到
unique_ptr
,首先用 alias declaration 定义了几个便于阅读的类型名称pointer
,element_type
和deleter_type
。其中pointer
是 deleter 使用的指针类型,默认为_Ty*
。
- 先忽略下面的众多函数,跳到
private
部分,发现 MSVC的std::unique_ptr
内部对 deleter 和数据指针实际以类似 pair 的形式存储:
unique_ptr
在使用默认 deleter 时能做到大小和裸指针一样的秘诀也在这个存储方式 compressed pair 中,推荐一篇博客:实现 compressed pair
- 构造函数:
_Zero_then_variadic_args_t
和_One_then_variadic_args_t
起标识作用,指示 pair 的第一项(deleter)是用默认构造函数还是用以第一个参数做参数的构造函数(通过std::forward
转发)。- 仅传入
_Ptr
的构造函数:deleter 用默认构造函数,pointer 用_Ptr
构造; - 传入
_Ptr
和 const 引用_Dt
的构造函数:deleter 用_Dt
构造,pointer 用_Ptr
构造; - 传入
_Ptr
和右值_Dt
的构造函数:deleter 用std::move(_Dt)
构造,pointer 用_Ptr
构造; - 传入
_Right
的移动构造函数:deleter 用_Right.get_deleter()
构造,pointer 用_Right.release()
(见下面)构造; - 复制构造函数(和复制赋值运算符)被屏蔽(
=delete
)。
- getter 函数:
get()
,operator->()
,operator*()
,get_deleter()
,直接返回_Mypair
中的项。 operator bool()
:转化为 bool 值,只能显式调用。
- 三个释放函数:
release()
:将自身内含指针设为nullptr,不销毁并返回原指针。通过std::exchange()
实现。reset()
:将自身内含指针销毁,设置为新的指针。先通过std::exchange
将_Old
指针换出来,如果_Old
非空,通过_Mypair._Get_first()
拿到 deleter,然后以_Old
为参数调用 deleter。- 析构函数:释放逻辑与
reset()
后半部分相同。