暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Item21 优先使用std::make_unique和std::make_shared来代替new

程序员的Cookbook 2020-08-10
863

    std::make_shared
是在 C++11
中添加的一个专门用来创建智能指针的方法,而不幸的是 std::make_unique
C++11
中并没有,直到 C++14
才引进来。不过实现它也是一件很容易的一件事,如下:

   上面的这个版本是不支持创建数组类型的智能指针,也不支持自定义删除器的。尽管如此,当你需要 make_unique
的时候,你也可以很简单的实现一个简易版本。

   还记得我们在之前是怎么创建智能指针的吗?, new
一个原始指针,然后用它初始化一个智能指针类型的变量即可,那么既然已经有了创建智能指针的方法,本文为何又要引入第二种方法呢?因为使用make系列函数创建智能指针要比原始方法创建有诸多优点,因此在实际使用过程中应该优先使用make系列函数来创建智能指针。那么到底有哪些优点呢,本文将会一一讲解。

  • 避免重复,代码更清晰,不容易引入不一致的代码导致bug。

   上面的代码在使用 new
创建智能指针的时候,你会发现类型出现了重复(出现了两次 Widget
),后面如果要修改类型的话,必须要全部修改,否则会导致代码的不一致,而使用 make
系列函数创建的智能指针就没有这个问题。

  • 异常安全

上面这行代码总共分成下面几个步骤:

  1. newWidget

  2. std::shared_ptr<Widget>
    构造函数

  3. computePriority

但是不幸的是这三行代码的执行顺序是未定义的,也就是什么样的顺序都可以,如果按照下面的顺序执行,那么就有可能导致资源泄漏。

  1. newWidget

  2. computePriority

  3. std::shared_ptr<Widget>

当步骤2的 computePriority
发生异常,这会导致步骤1分配的内存无法得到回收,如果在这里使用make系列函数创建的话就不会出现这种问题。

  • 避免多次内存分配,提升性能


       还记得在上一节中谈到的 shared_ptr
    控制块
    的概念吗?,总的来说一个智能指针包含了两个部分一个部分是分配的堆内存,另外一个部分就是控制块。通常情况下我们使用下面的方法创建智能指针的时候,其实就进行两次内存分配的操作,一次是分配对象的内存,另外一次则是分配控制块所需要的内存。

如果是使用 make_shared
就不会存在这个问题, make_shared
会一次性分配对象和控制块所需要的内存。

   上文中谈到了诸多 make_shared
的优点和好处,直觉上我们就应该无论如何都使用 make_shared
来创建智能指针,这个时候我不得不站出来谈谈 make_shared
的缺点,避免给大家造成一定要使用 make_shared
的错觉,正如本文标题一样,应该是优先使用,只有清楚的认识到 make_shared
的缺点才能更好的使用 make_shared

  • 无法自定义删除器


       第一个缺点很明显那就是在使用make_shared创建的智能指针的时候我们是没办法自定义删除器的,这是一个很遗憾的地方,我觉得在 C++17
    可能会解决这个问题。


  • 使用 make_shared
    会带来语法上的歧义

上面的代码是什么含义呢?,创建10个元素,每个元素的值是20,还是创建两个元素分别是10和20。对应于下面的代码:

   区别就在于使用的是括号,还是花括号。好在 make_shared
使用的是括号的语义,那如何实现第二种语义呢?很可惜 make_shared
做不到。这个时候只能使用 new
的方式来创建。

  • 最后一个缺点但也是最重要的一个缺点,延长了对象销毁的时机


       智能指针在引用计数变为0的时候会进行对象的析构,但是如果你使用了 make_shared
    创建智能指针的话,即使引用计数变为0对象也不会析构,这一切的根本原因就是 std::weak_ptr
    ,为了能让 weak_ptr
    可以探查对象的生死,在智能指针的控制块中会保存 weak_ptr
    相关的信息,以便 weak_ptr
    可以探查对象的生死,就是因为这个原因导致引用计数变为0的时候,对象是析构了,但是控制块仍然存在,直到所有的 weak_ptr
    都销亡了,控制块才会释放。那么问题就来了,使用 make_shared
    创建的智能指针其控制块和对象所占用的内存是放在一起的,只能一起释放,无法释放其中的一部分。这也导致了对象的生命周期被拉长,直到所有的 weak_ptr
    都析构为止。

   谈完了优缺点或许你开始迷惑,究竟该如何在两种方式中选择适合自己的,不希望对象的生命周期被拉长,又不希望有异常安全的问题。对于异常安全的问题来说其实很好规避。

   将创建智能指针的步骤独立起来,这样就不存在异常安全的问题了,但是上面的方法还有另外一个问题就是,会造成一点点性能上的问题。会造成智能指针拷贝的开销。std::shared_ptr<Widget>(newWidget)

   这种写法的情况下,这是一个右值,默认会进行移动,这样就避免了拷贝,如果改成上面的方式, spw
是一个左值那么就必须进行拷贝复制了,为此可以将其转换为右值进程移动,避免这次拷贝。

到此为止,一个异常安全的,性能上也没有什么损失的方法就有了。

Tips

  1. 相比于直接使用new,make系列的函数消除了源代码重复、提升了异常安全性,并且 std::make_shared
    和 std::allocate_shared
    生成的代码更小更快

  2. 对于希望自定义删除器以及通过{}进行初始值的设定时,不适合使用make系列函数

  3. 对于 std::shared_ptr
    来说有两类场景不适合使用 make
    系列函数,第一个就是需要自定义管理内存的,第二个就是管理大对象时,并且存在 std::weak_ptr
    比 std::shared_ptr
    生命周期更长的情况


文章转载自程序员的Cookbook,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论