[译]GotW #89 Smart Pointers

时间:2022-09-07 20:09:07

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;

[译]GotW #89 Smart Pointers

我们想要避免两次分别的分配动作。如果选择使用make_ptr去分配对象,那么shared_ptr所有的只会有一个。然后它的实现可能将他们折叠在一起到一个分配当中。如下面代码和图所示。

// Example 2(b): Single allocation
auto sp1 = make_shared<widget>();
auto sp2 = sp1;

[译]GotW #89 Smart Pointers

组合分配有两个主要好处:
     · 减少了分配开销,包括内存碎片。首先,这个最明显的方式是通过减少分配请求的次数,分配一般是属于昂贵的操作。这同时也减少了在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或者两个你都不知道有。

[译]GotW #89 Smart Pointers的更多相关文章

  1. &lbrack;c&plus;&plus;&rsqb; Smart Pointers

    内存管理方面的知识 基础实例: #include <iostream> #include <stack> #include <memory> using names ...

  2. &lbrack;译&rsqb;GotW &num;6b Const-Correctness&comma; Part 2

         const和mutable对于书写安全代码来说是个很有利的工具,坚持使用它们. Problem Guru Question 在下面代码中,在只要合适的情况下,对const进行增加和删除(包括 ...

  3. &lbrack;译&rsqb;GotW &num;6a&colon; Const-Correctness&comma; Part 1

    const 和 mutable在C++存在已经很多年了,对于如今的这两个关键字你了解多少? Problem JG Question 1. 什么是“共享变量”? Guru Question 2. con ...

  4. &lbrack;译&rsqb;GotW &num;4 Class Mechanics

    你对写一个类的细节有多在行?这条款不仅注重公然的错误,更多的是一种专业的风格.了解这些原则将会帮助你设计易于使用和易于管理的类. JG Question 1. 什么使得接口“容易正确使用,错误使用却很 ...

  5. &lbrack;译&rsqb;GotW &num;3&colon; Using the Standard Library &lpar;or&comma; Temporaries Revisited&rpar;

    高效的代码重用是良好的软件工程中重要的一部分.为了演示如何更好地通过使用标准库算法而不是手工编写,我们再次考虑先前的问题.演示通过简单利用标准库中已有的算法来避免的一些问题. Problem JG Q ...

  6. &lbrack;译&rsqb;GotW &num;2&colon; Temporary Objects

        不必要的和(或)临时的变量经常是罪魁祸首,它让你在程序性能方面的努力功亏一篑.如何才能识别出它们然后避免它们呢? Problem JG Question: 1. 什么是临时变量? Guru Q ...

  7. &lbrack;译&rsqb;GotW &num;1&colon; Variable Initialization

    原文地址:http://herbsutter.com/2013/05/09/gotw-1-solution/ 第一个问题强调的是要明白自己在写什么的重要性.下面有几行简单的代码--它们大多数之间都有区 ...

  8. &lbrack;译&rsqb;GotW &num;5:Overriding Virtual Functions

       虚函数是一个很基本的特性,但是它们偶尔会隐藏在很微妙的地方,然后等着你.如果你能回答下面的问题,那么你已经完全了解了它,你不太能浪费太多时间去调试类似下面的问题. Problem JG Ques ...

  9. &lbrack;译&rsqb;GotW &num;1&colon; Variable Initialization 续

    Answer 2. 下面每行代码都做了什么? 在Q2中,我们创建了一个vector<int>且传了参数10和20到构造函数中,第一种情况下(10,20),第二种情况是{10, 20}. 它 ...

随机推荐

  1. 关于android获得设备宽高

    传统的办法: DisplayMetrics dm = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(d ...

  2. Proxy代理(AOP实现原理)

    AOP基于动态代理实现:返回代理对象 java.lang.reflect 包Proxy 类,构造代理类.newProxyInstance() 就是创建代理对象的方法. Proxy.newProxyIn ...

  3. nginx图片服务器配置

    worker_processes ; #error_log logs/error.log; #error_log logs/error.log notice; #error_log logs/erro ...

  4. &period;NET方面的框架的整理和总结

    自从学习.NET以来,优雅的编程风格,极度简单的可扩展性,足够强大开发工具,极小的学习曲线,让我对这个平台产生了浓厚的兴趣,在工作和学习中也积累了一些开源的组件,就目前想到的先整理于此,如果再想到,就 ...

  5. 《DSP using MATLAB》Problem 7&period;26

    注意:高通的线性相位FIR滤波器,不能是第2类,所以其长度必须为奇数.这里取M=31,过渡带里采样值抄书上的. 代码: %% +++++++++++++++++++++++++++++++++++++ ...

  6. HBuilder的扩展插件开发暴露了一个事实:其实不能实现写一次代码实现跨平台App生成

    HBuilder的扩展插件开发,原来并不能生成单独的插件jar,而是以源码 - 类的形式进行开发,这其实就要求必须使用离线打包. 事实上,开发顺序应该是:先弄好离线打包框架,然后在AS里进行扩展插件开 ...

  7. Lotto HDU

    链接 [http://acm.hdu.edu.cn/showproblem.php?pid=1342] 题意 分析 DFS 代码 #include<cstdio> #include< ...

  8. maven 父工程 消除重复 对子模块进行管理 主要继承依赖

     子类继承父类的 可以不需要groupid与versionid

  9. py3&plus;requests&plus;urllib&plus;bs4&plus;threading,爬取斗图图片

    实现原理及思路请参考我的另外几篇爬虫实践博客 py3+urllib+bs4+反爬,20+行代码教你爬取豆瓣妹子图:http://www.cnblogs.com/UncleYong/p/6892688. ...

  10. python里的函数

    map()是 Python 内置的高阶函数,它接收一个函数 f 和一个 list,并通过把函数 f 依次作用在 list 的每个元素上,得到一个新的 list 并返回. 假设用户输入的英文名字不规范, ...