Std::unique_ptr
本文最后更新于:April 20, 2022 am
本篇为智能指针系列第一篇,介绍std::unique_ptr
系列导航:智能指针
unique_ptr:独享所有权
当希望使用智能指针时,std::unique_ptr
通常就是首选。
性能
默认情况下,可以认为std::unique_ptr的大小和裸指针一致,并且对于大多数操作而言(包括解引用dereferencing
),它们俩都会执行相同的指令。这意味着即使在内存和时钟周期紧张的情况下,也可以使用之。如果一个裸指针对于你而言已经足够轻巧和快速,那么std::unique_ptr
同样会如此。
语义
std::unique_ptr
内置的语义是独占式拥有权。一个non-null
的std::unique_ptr
总是拥有其指向的资源。对一个std::unique_ptr
对象执行移动操作可以将管理权移交给另一个std::unique_ptr
对象,同时将源对象置空。
拷贝构造函数是不被允许的,在源码中也已经使用=delete
将之禁用,毕竟拷贝操作明显违背了类设计的初衷。如果拷贝成功,将会有两个std::unique_ptr
对象指向同一段内存区域,并且每个对象都认为它拥有这段内存,内存也会被析构两次。显然,std::unique_ptr
是一个只支持移动的类型。在析构时,std::unique_ptr
会对其管辖的资源做回收操作。默认情况下,std::unique_ptr
调用delete
操作符。
使用举例
std::unique_ptr
的一种通常的使用方式是,将之作为在对象继承体系中的一个工厂函数的返回值。参看如下继承体系:
classDiagram
Investment <|-- Stock
Investment <|-- Bond
Investment <|-- RealEstate
对于此体系而言,一个工厂函数在堆上分配一个对象,并且返回指向其的指针,并期待调用者在合适的时候将其释放掉。仔细想想会发现这个场景正是std::unique_ptr
的用武之地,因为这里是调用者获得了对象的管理权,并且std::unique_ptr
能够在自身析构时自动释放其管理的对象。举例而言,此处的工厂函数可以写为
1 |
|
调用者:
1 |
|
实际使用的场景也可以变得更复杂一些,比如此处工厂函数返回的std::unique_ptr
被移动到某个容器类中,并且这个容器被移动到了某个对象的内部成员变量上,并且此对象将会在稍微晚些的时候被析构。这种情况下,std::unique_ptr
对象依旧会被析构,其管理的内容也会被释放[1]。
如果在过程中,拥有权转移链因为异常,或者不寻常的控制流(函数提前返回,或者在循环中执行了break
)被打断,std::unique_ptr
的析构和其管理的资源的释放也会被保障。
自定义deleter
默认情况下,释放资源会通过调用delete
操作符实现。但是在std::unique_ptr
对象构造的过程中,也可以手动给它指定一个自定义的资源释放函数(任意函数,或者函数对象,或者lamda表达式),它会在合适的时机调用传入的函数。
举例来说,在这个场景下,如果需要makeInvestment
函数所创建的对象,在其释放管理的资源时先打印一句日志,就可以这样来写:
1 |
|
有几点需要说明:
delInvmt
是一个用户自定义的deleter
。所有的自定义的deleter
的参数都是一个指向待销毁的对象的裸指针。函数可以做一些自定义的内容,但别忘了调用delete
操作符。使用
lamda
表达式创建delInvmt
更方便,并且正如后面会讨论的,这也比手写一个传统类型的函数也更高效。当使用一个自定义的
deleter
时,需要将deleter
的类型也传给std::unique_ptr
用于模板参数。记住这一点,这和std::shared_ptr
很不同!基本的实现思路是,先创建一个绑定了资源释放函数的空的
std::unique_ptr
,再调用reset
让其指向一个适当的对象。在第八行里,我们通过将lamda
表达式作为std::unique_ptr
的构造函数的第二个参数,建立其之间的关联。直接向
std::unique_ptr
对象赋予一个裸指针是被禁止的,因为这涉及隐式构造另一个std::unique_ptr
对象。C++11禁止了这种操作。此处应该使用reset
。每次调用
new
操作符,我们都使用了std::forward
(Item25)来将传入的参数完美转发给makeInvestment
。这使得函数调用者提供的所有信息都被一字不差地转发给了我们正在调用的构造函数。我们在自定义的
deleter
中,统一使用Investment*
的方式来delete
资源,而不管传进来的究竟是哪种子类的指针。熟悉C++
的面向对象编程的朋友会知道,此时在继承体系中所有类的析构函数都应该被声明为虚函数。1
2
3
4class Investment{
public:
virtual ~Investment();
}
在调用者的视角来看上述代码,我们只需要在构造时传入类型参数和释放资源函数,不用在意资源在被释放期间需要额外的处理,也不用关心什么时候应该销毁资源,更不用花费心思确保销毁的代码只被调用一次。
在C++14中,支持函数返回值类型推导(Item3),这意味着makeInvestment
可以被实现得更简单:
1 |
|
上文提到,可以认为std::unique_ptr
的大小和裸指针一样,这在自定义delete
的情况下不是普遍成立。当一个函数指针作为deleter
,这会使得std::unique_ptr
实例的大小从1个字涨到2个字。对于函数对象形式的deleter
,大小上的开销增量取决于对象内部是否有存储其它状态。无状态的函数对象(比如不捕获内容的lamda表达式)不会额外引起其它的存储开销。当函数对象作为deleter
,并且这个对象有很多状态时,这会使得std::unique_ptr
的大小增大很多。
1 |
|
其它使用情景
工厂函数并不是std::unique_ptr
的唯一使用场景。std::unique_ptr
更被经常用于
实现Pimpl
(Item22)。
std::unique_ptr
有两种形式,一种是对于单独的对象(std::unique_ptr<T>
),另一种是数组形式std::unique_ptr<T[]>
,因此,std::unique_ptr
在指向类型上是清晰的。同时,这两种形式都分别重载了不同的操作符,以方便使用,比如前者没有重载[]
运算符,而后者没有重载*
(解引用)运算符。
这得一提的是,不建议使用std::unique_ptr
的数组形式,原因很简单,因为有更好的容器类型(std::array
, std::vector
, std::string
, etc)。对于数组形式的std::unique_ptr
,能想到的几乎唯一的用武之地,可能就是已经通过一个C
风格的API
取得了一个指向堆上的数组的指针时。
std::unique_ptr
是C++11用来表示独占所有权的方式,但是它的一个最吸引人的地方是,它可以非常简单并且高效地被直接转换成一个std::shared_ptr:
1 |
|
这也是为什么std::unique_ptr
如此适合工厂函数的原因,因为工厂函数不明白调用者究竟需不需要让资源是独占的,但是工厂函数返回的std::unique_ptr
实例总是最高效的,并且在std::unique_ptr
类的设计上,也没有阻值它被转换成更灵活的std::shared_ptr
类型。
总结
std::unique_ptr是一种小型,快速,只能移动的智能指针,管理资源时有独占式语义
默认情况下,资源的释放通过调用delete操作符实现,但也可以自定义deleter。有状态的deleter和函数指针都会引起std::unique_ptr实例的内存占用增加
从std::unique_ptr到std::shared_ptr的转换极为简单
- 个别例外并不遵循这个规则,且大多数是由程序异常结束引起的。如果一个异常传递到了线程的第一个函数(比如main)以外,或者
noexcept
声明被打破(见Item14),局部对象可能不会被析构。如果调用了std::abort,或者某个退出函数(std::exit, std::quick_exit, std::_Exit等等),那局部对象肯定不会被析构。 ↩
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!