[译]GotW #89 Smart Pointers
There’s a lot to love about standard smart pointers in general, and unique_ptr in particular.
Problem
JG Question
1.什么时候你应该使用shared_ptr vs unique_ptr?尽可能列出你所想到的注意事项。
Guru Question
2.为什么你应该总是使用make_shared来创建一个被shared_ptr(s)拥有的对象?请解释。
3.为什么你应该总是使用make_unique来创建一个被unique_ptr拥有的对象?请解释。
4.怎么处理auto_ptr?
Solution
1.什么时候你应该使用shared_ptr vs unique_ptr?尽可能列出你所想到的注意事项。
当存在疑问时,默认情况下优先使用unique_ptr。如果有需要的话可以以后再改为shared_ptr。如果你从一开始就知道你所需要的共享所有权类型指针,那么可以直接通过make_shared来声明shared_ptr类型(查看下面的#2)。
有三个理由解释为什么“当存在疑问时,默认情况下优先使用unique_ptr”。
首先,使用最简单的语义应该是足够的:选择正确的智能指针来最直接表达你的意图和你(现在)所需要的。如果你创建了一个新类型,但是不知道最终是否需要共享对象所有权,使用unique_ptr来表达唯一的所有权。或者你可以将它放入容器中(例如:vector<unique_ptr<widget>>),然后做你想要用原始指针(raw pointer)做的事情,不会有问题,除了很安全。如果之后需要共享所有权,你可以直接将unique_ptr改变为shared_ptr。
其次,unique_ptr比shared_ptr更高效。unique_ptr不需要去维护引用计数信息和底层的控制块。它设计的目的就是尽可能简单便宜地像原始指针一样移动和使用。当你没有要求比你需要的多,那么你就没有必要去承受不使用那部分带来的开销。
最后,使用unique_ptr更加灵活且有保持选择的权利。如果一开始就使用unique_ptr,你之后可以总是转换到shared_ptr或者通过.get()或.release()来使用其他自定义的智能指针(甚至是原始指针)。
Guideline:优先使用标准的智能指针。默认情况下使用unique_ptr,如果需要共享则使用shared_ptr。它们在C++标准库中属于常见类型。只有在和其他库进行交互的必要前提下才使用其他智能指针类型,或使用标准的智能指针的deleters和allocators达不到你的需求的自定义行为。
2.为什么你应该总是使用make_shared来创建一个被shared_ptr(s)拥有的对象?请解释。
注意:如果你需要是同自定的allocator来创建一个对象,你可以使用allocate_shared,这几乎很少见。尽管它的名字稍微有点不一样,但allocate_shared应该被视为“可以让你指定一个allocator风味的make_shared”。所以在这主要讨论make_shared而并不会去区分它们之间的区别。
有两种主要情况你不能使用make_shared去创建一个被shared_ptr(s)拥有的对象:(a)你需要使用自定义的deleter,这种情况下因为使用shared_ptr来管理非内存资源或在非标准内存区分配一个对象,你不能使用make_shared因为它不支持指定的deleter;(b)如果你采取一个原始指针(raw pointer)指向一个来需要处理的自其他(通常是legacy code)代码的对象,你可以直接用原始指针去构建shared_ptr。
Guideline:使用make_shared(或需要自定义allocator的allocate_shared)去创建你知道需要使用shared_ptr类型的对象,除非你需要一个自定义的deleter或从采用一个来自其他地方的原始指针(raw pointer)。
那么,为什么几乎总是尽可能地使用make_shared(或需要自定义allocator的allocate_shared)?这里有两个主要的原因:简单和高效。
第一,使用make_shared的代码更简单,书写清晰和正确的代码是第一位。
第二,使用make_shared更高效。shared_ptr需要在控制块维护一个被所有share_ptr(s)和weak_ptr(s)引用的指定对象的内务信息(housekeeping information)。这个内务信息包含了不止一个引用计数,而是两个。
· 一个“强引用”来跟踪当前保持活动的shared_ptr(s)的个数。当最后一个强引用消失时这个共享对象就会被销毁(可能会被回收)。
· 一个“弱引用”来跟踪当前观察对象的weak_ptr(s)的个数。当最后一个弱引用消失时,这个共享的内务控制块会被销毁和回收(如果还没有被回收话)。
如果你分配对象分别通过原始的new表达式,然后再传给shared_ptr,那么shared_ptr会没有选择只能分别去分配控制块,如下面代码和图所示。
// Example 2(a): Separate allocation auto sp1 = shared_ptr<widget>{ new widget{} }; auto sp2 = sp1;
我们想要避免两次分别的分配动作。如果选择使用make_ptr去分配对象,那么shared_ptr所有的只会有一个。然后它的实现可能将他们折叠在一起到一个分配当中。如下面代码和图所示。
// Example 2(b): Single allocation auto sp1 = make_shared<widget>(); auto sp2 = sp1;
组合分配有两个主要好处:
· 减少了分配开销,包括内存碎片。首先,这个最明显的方式是通过减少分配请求的次数,分配一般是属于昂贵的操作。这同时也减少了在allocators上的竞争;其次,只使用一块内存而不是两块可以减少每次分配的开销。当向系统要求一块内存时,系统一般至少会给出一些字节,通常还会给出更多一些,因为里面维护其他的信息需要一定的空间。因此通过使用单个内存块,
倾向于减少额外的开销,最终减少因为零碎的内存区而导致的内存碎片。
· 改善了区域(locality)。引用计数经常会被对象使用,对于小的对象很可能是处在相同的内存线(cache line)上,这个可以提高缓存性能(只要当中没有一些线程将智能指针拷贝到一个紧密的循环中,不要那么做)。
像往常一样,如果能试图通过一个简单的函数调用来表达更多的意图,那么系统会找到一个更高效的方式来完成任务。这个可以通过往vector中填充元素可以看出,因为使用v.inser(first,last)填充100个元素远比调用100次v.inser(value)更高效。所以使用单个make_shared调用比分别调用new widget()和shared_ptr(widget*)更好。
此外还有两个好处是:使用make_shared避免了显示使用new和避免了一些异常安全问题。上述这些同样适用于make_unique。
3.为什么你应该总是使用make_unique来创建一个被unique_ptr拥有的对象?请解释。
和make_shared一样。存在两个主要情况不能使用make_unique分配一个unique_ptr所有权类型的对象:如果需要一个deleter,或采用一个原始指针(raw pointer)。
否则,尽可能地优先使用make_unique。
Guideline: 使用make_unique创建一个非共享的对象,除非需要一个自定义的deleter或采用一个来自于其他地方的原始指针。
因为make_shared和make_unique有对称性,所以和上述一样。首先,优先使用make_unique<T>()而不是繁琐的unique_ptr<T>{ new T{}},因为通常情况下应该避免显示使用new
Guideline:不要显示使用new,delete,和*指针。除非需要实现一些底层的数据结构这种罕见的情况。
其次,它避免了使用原始new的一些异常安全问题。下面是个例子:
void sink( unique_ptr<widget>, unique_ptr<gadget> ); sink( unique_ptr<widget>{new widget{}}, unique_ptr<gadget>{new gadget{}} ); // Q1: do you see the problem?
简单说就是,如果首先分配和构建new widget,然后再分配或构建new gadget时发生了异常,那么widget则会泄露。有人可能会像:“这样的话,那么可以调整new widget{}到make_unique<widget>()这样就没问题了对吧?”也就是:
sink( make_unique<widget>(), unique_ptr<gadget>{new gadget{}} ); // Q2: is this better?
答案是否定的。因为C++在评估函数参数的序列是未指定的,因此new widget或new gadget都可能先执行。
因此,只是改变参数中的一个都没有堵上这个漏洞,只有两者都使用make_unique才真正完全消除了这个问题:
sink( make_unique<widget>(), make_unique<gadget>() ); // exception-safe
关于异常安全的问题会在GotW #56更多的讨论。
Guideline:分配对象时,默认情况下优先选择使用make_unique。当知道一个对象的生命期需要通过shared_ptr(s)进行管理,那么使用make_shared。
4.怎么处理auto_ptr?
auto_ptr在C++有移动语义前有很多特性是作为去创建一个和unique_ptr的意思。auto_ptr现在是废弃了的。不应该在新写的代码中使用它。
如果在既存代码中使用了auto_ptr,那么可以进行全局搜索然后用unique_ptr去替换它。绝大多数的使用都会工作的一样,可能会暴露一些问题(编译错误)或修复了(轻微)一些bug或者两个你都不知道有。