Item21 Perfer std::make_unique and std::make_shared to direct use of new

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

template<typename T, typename... Ts>
std::unique_ptr<T> make_unique(TS&&... params)
{
  return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}

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

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

  • 避免重复,代码更清晰,不容易引入不一致的代码导致bug。
auto upw1(std::make_unique<Widget>());
std::unique_ptr<Widgte> upw2(new Widget); //不能使用auto,new Widget返回的是原始指针类型

auto spw1(std::make_shared<Widget>());
std::shared_ptr<Widget> spw2(new Widget);

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

  • 异常安全
processWidget(std::shared_ptr<Widget>(new Widget), computePriority);

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

  1. new Widget
  2. std::shared_ptr<Widget>构造函数
  3. computePriority

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

  1. new Widget
  2. computePriority
  3. std::shared_ptr<Widget>

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

  processWidget(std::make_shared<Widget>(), computePriority());
  • 避免多次内存分配,提升性能

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

  std::shared_ptr<Widget> p(new Widget);

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

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

  • 无法自定义删除器

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

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

auto spv = std::make_shared<std::vector<int>>(10, 20);

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

  std::vector<int> p(10, 20);   //第一种语义 10个元素,每个元素的值是20
  std::vector<int> p2{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> spw(new Widget);
processWidget(spw, computePriority());

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

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

std::shared_ptr<Widget> spw(new Widget);
processWidget(std::move(spw), computePriority());

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

©️2020 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值