原文链接:http://herbsutter.com/2013/05/29/gotw-89-solution-smart-pointers/
标准智能指针有诸多令人喜爱的地方,尤其是unique_ptr。
问题
学徒提问
1. shared_ptr和unique_ptr应该分别在什么时候使用?尽可能多列举出一些需要考虑的因素。
大师提问
2. 为什么你应该总是使用make_shared来创建被shared_ptr持有的对象?请解释说明。
3. 为什么你应该总是使用make_unique来创建一个最初被unique_ptr持有的对象?请解释说明。
4. auto_ptr到底是怎么回事?
解答
1. shared_ptr和unique_ptr应该分别在什么时候使用?
如果不确定, 那么默认情况下请优先选择unique_ptr,如果以后有需要的话,你总是可以将unique_ptr移动转换为shared_ptr。但如果你从最初就知道你需要的是共享的所有权,那就通过make_shared(参见下面#2)创建使用shared_ptr。
"如果有疑问, 优先选择unique_ptr"有如下三个理由:
首先, 只要够用,就使用最简单的语意: 选择最能直接表达你意图和你(目前)最需要的智能指针。如果你要创建一个新对象,而且并不知道你最终需要的是共享所有权,那就使用表示专享所有权的unique_ptr。你依然可以将它放在容器当中(e.g., vector<unique_ptr<widget>>) 或者完成大部分你可以用裸指针完成的工作,而且还更安全。如果在以后你需要共享所有权,你总是可以将unique_ptr移动转换为一个shared_ptr。
其次,unique_ptr的效率比shared_ptr更高。一个unique_ptr不需要维持引用计数信息和使用额外的control block,并且,移动和使用unique_ptr的代价和用裸指针同样的低廉。如果你不需要多余的功能,你就不必为那些你不需要的功能复出额外的代价。
再次,最初使用unique_ptr会更加灵活,而且以后你还可以改用其它的。如果你最初使用unique_ptr,以后可以通过移动转化成shared_ptr,或者是其它自定义的智能指针(甚至是裸指针)。
Guideline: 优先使用标准智能指针, 默认用unique_ptr,如果需要共享所有权就用shared_ptr。这些是所有C++库都能理解的常见类型。只有在需要和其他库互操作的时候,或者是需要特殊的行为,并且无法通过自定义标准智能指针的deleter和allocator的时候,才使用其他的智能指针类型。
2. 为什么你应该总是使用make_shared来创建被shared_ptr持有的对象?请解释说明。
注意:如果你需要使用自定义的allocator来创建对象, 这种情况很少见, 你可以使用allocate_shared。虽然名称稍有不同,但我们应该将allocate_shared看做“可以指定allocator的make_shared”, 所以下面的讨论不会对这两者做过多的区分。
在两种情况下,即使你知道你的对象需要由shared_ptr管理,但也不能用make_shared (或allocate_shared):(a) 如果你需要自定义的deleter,例如使用shared_ptrs管理非内存的资源或者是分配在非标准内存区域的对象,你不能使用make_shared因为它不能指定deleter;(b) 如果你要管理一个从其他代码(通常都是遗留代码)传来的裸指针,你就要直接位那个裸指针针创建一个shared_ptr。
Guideline: 用make_shared (或者,如果你需要自定义的allocator,allocate_shared) 来创建一个你知道需要被shared_ptr管理的对象,除非你需要用自定义的deleter或者是要管理从别的地方传来的裸指针。
那么,为什么要尽可能的使用make_shared (或者,如果你需要自定义的allocator,allocate_shared) 呢?主要有两个理由:简洁,以及高效。
第一点,用make_shared会让代码更简洁。写代码首先要注重清晰度和正确性。
第二点,用make_shared更高效。shared_ptr的实现当中需要在一个control block中维持一些由所有指向同一对象的shared_ptrs和weak_ptrs共享的簿记信息。具体来说,这个簿记信息当中需要保存的不单单只是一个,而是两个引用计数:
- 一个“强引用”计数来追踪当前保持对象存活的所有shared_ptrs的数目。这个共享的对象在最后一个强引用失效之后被销毁(可能同时内存也被回收)。
- 一个 “弱引用”计数来追踪当前观察这个对象的所有weak_ptrs的数目。共享的簿记control block在最后一个弱引用失效后被销毁并且内存也被回收(共享对象的内存如果还没被回收,这时候也会被回收)。
如果你通过new表达式来单独的分配对象,然后将它传给一个shared_ptr,shared_ptr的实现就只好单独的分配control block,如Example 2(a) and Figure 2(a)所示。
1 // Example 2(a): Separate allocation 2 auto sp1 = shared_ptr<widget>{ new widget{} }; 3 auto sp2 = sp1;
Figure 2(a): Approximate memory layout for Example 2(a).
我们希望在这里避免两次单独的内存分配。如果你使用make_shared一次性的分配对象和shared_ptr,那么shared_ptr的实现就可以把它们合并成一次内存分配,如Example 2(b) and Figure 2(b)所示。
1 // Example 2(b): Single allocation 2 auto sp1 = make_shared<widget>(); 3 auto sp2 = sp1;
Figure 2(b): Approximate memory layout for Example 2(b).
需要注意的是,合并内存分配有两个主要的优点:
- 减少内存分配的开销,包括内存碎片化。首先,最明显的一点是减少了分配内存请求的次数,这通常是开销更大的操作。合并内存分配还可以缓解对内存分配器的竞争(一些分配器伸缩性不是那么好)。其次,只用一块,而不是两块内存可以减少每次分配的开销。每次你请求一块内存的时候,系统都给你至少那么些字节,通常还会多一些,因为可能使用了固定尺寸的内存池或者要记录每次分配的信息。所以通过只用一块内存,我们很可能会减少总体的额外开销。最后,我们还会减少那些处于已分配的内存块之间,导致了内存碎片化的“死”内存。
- 增加了局部性。引用计数会频繁的同对象一起被用到,对小对象来说,他们很有可能会在相同的缓存行上,这就提高了缓存的性能(只要没有其它线程在一个忙碌的循环当中一直复制这个智能指针;别这么干)。
一如既往,当你可以通过单一的函数调用将你想要做的事情表述的更清楚,系统就更有可能找到更高效的方式来完成这项工作。这就好比插入100个元素到vector当中时应该用一个v.insert( first, last )而不是使用100个v.insert( value )一样,你应该用一个make_shared而不是分表调用new widget()和shared_ptr( widget* )。
还有两个优点:用make_shared避免了显示调用new并且避免了异常安全问题。这两点对make_unique也适用,我们在#3中会讲到它们。
3. 为什么你应该总是使用make_unique来创建一个最初被unique_ptr持有的对象?请解释说明。
和make_shared一样,在两种情况下你无法用make_unique创建一个你知道(至少是最初)会被unique_ptr持有的对象:如果你需要一个自定义的deleter,或者管理一个已有的裸指针。
否则,几乎是大部分时候,优先使用make_unique。
Guideline: 用make_unique创建非共享(至少目前不会被共享)的对象,除非需要用自定义deleter或者需要管理其它地方传来的裸指针。
除了和make_shared一样的地方,make_unique还提供了另外两个优点。第一,你应该优先选择make_unique<T>()而不是更加啰嗦的unique_ptr<T>{ new T{} },因为你通常应该避免显示的调用new。
Guideline: 不要显示的使用 new, delete, 和持有裸指针,除了极少情况下需要在底层的数据结构实现当中需要对这些进行封装。
第二,这避免了一些已知的由裸new带来的异常安全问题。下面是个例子:
1 void sink( unique_ptr<widget>, unique_ptr<gadget> ); 2 3 sink( unique_ptr<widget>{new widget{}}, 4 unique_ptr<gadget>{new gadget{}} ); // Q1: do you see the problem?
简单来说,如果你首先分配并构造new widget,然后在分配或者构造new gadget的时候抛出了一个异常,widget就泄露了。你可能会想:“嗯,我可以将new widget{}改为make_unique<widget>(),问题就解决了,对吧?”就是说:
1 sink( make_unique<widget>(), 2 unique_ptr<gadget>{new gadget{}} ); // Q2: is this better?
答案是No,因为C++没有指定函数参数的估值顺序,所以new widget和new gadget的估值顺序是不确定的。如果先分配和构造new gadget,那么当make_unique<widget>抛出异常的时候,问题还是一样的。
只是将其中一个参数转化为make_unique是不能补上这个漏洞的,但当我们将两个参数都改为使用make_unique的时候,就可以完全消除这个问题:
1 sink( make_unique<widget>(), make_unique<gadget>() ); // exception-safe
异常安全问题GotW #56当中会更详尽的阐述。
Guideline: 分配对象的时候,首选make_unique,如果知道对象应该由shared_ptr管理,就用make_shared。
4. auto_ptr到底是怎么回事?
在C++有移动语意之前,auto_ptr说的好听点就是对建立unique_ptr的一次勇敢尝试。auto_ptr现在被弃用了,并且在新代码中不应该再使用它了。
如果你现有的代码库当中已经在使用auto_ptr了,有机会的时候就来一次全局的查找替换把auto_ptr改为unique_ptr;大多数情况下两者的作用相同,替换成unique_ptr可能会曝几个(编译错误)或是(悄悄地)修复几个你自己都不知道有的bug。
Acknowledgments
Thanks in particular to the following for their feedback to improve this article: celeborn2bealive, Andy Prowl, Chris Vine, Marek.