条款21:尽量使用std::make_unique和std::make_shared而不直接使用new(总结)

<1>关于make_unique 的几个关键点:
1. make_unique 同 unique_ptr 、auto_ptr 等一样,都是 smart pointer,可以取代new 并且无需 delete pointer,有助于代码管理。
2. make_unique 创建并返回 unique_ptr 至指定类型的对象,这一点从其构造函数能看出来。make_unique相较于unique_ptr 则更加安全。
3. 编译器不同,make_unique 要求更新。(std::make_shared是c++11的一部分,但std::make_unique不是,它是在c++14里加入标准库的)。
<2>make_unique 与make_shared 的知识介绍 :        
       std::make_unique 和 std::make_shared是三个make函数中的两个,make函数用来把一个任意参数的集合完美转移给
 一个构造函数从而生成动态分配内存的对象,并返回一个指向那个对象的灵巧指针
。第三个make是std::allocate_shared。
它像std::make_shared一样,除了第一个参数是一个分配器对象,用来进行动态内存分配。
        优先使用make函数的原因:

避免代码重复。考虑如下代码:

auto upw1(std::make_unique<Widget>());     // with make func
std::unique_ptr<Widget> upw2(new Widget); // without make func

auto spw1(std::make_shared<Widget>());     // with make func
std::shared_ptr<Widget> spw2(new Widget); // without make func

         使用new的版本重复了被创建对象的键入,但是make函数则没有。重复类型违背了软件工程的一个重要原则:应该避免代码重复,代码中的重复会引起编译次数增加,导致目标代码膨胀,最终产生更难以维护的代码,通常会引起代码不一致,而不一致经常导致bug产生。另外,输入两次比输入一次要费力些,谁都想减少敲键盘的负担。

②优先使用make函数的第二个原因是和异常安全有关。假设我们有个函数来根据一些优先级处理一个Widget对象:

void processWidget(std::shared_ptr<Widget> spw, int priority);

 现在我们假设有个函数来计算相对优先级:
 int computePriority();
 我们在一个调用processWidget时使用new而不使用std::make_shared:

processWidget(std::shared_ptr<Widget>(new Widget), 
              computePriority());       // potential resource leak!

上述代码会造成内存泄漏,分析如下:

       这和编译器翻译代码到目标代码有关。在运行期,传递给函数的参数必须先计算,然后才发生函数调用。因此在调用processWidget时,下面的事情必须在processWidget开始执行前发生:
1.表达式"new Widget"必须先计算,一个Widget对象必须先创建在堆上;
2.负责new出来的对象指针的std::shared_ptr<Widget>的构造函数必须执行;
3.computePriority必须运行。
       但编译器编写者并没有要求编译器必须按这样的顺序来生成代码。“new Widget”必须在 std::shared_ptr构造函数前被调用,因为new的结果用作构造函数的参数,但是computePriority可以在上述调用前、后或者中间执行。也就是说编译器可能会产生这样的代码来按如下顺序执行:
1.执行“new Widget”;
2.执行computePriority;
3.调用std::shared_ptr构造函数。
      假如这样的代码生成,在运行期,computePriority产生了异常,那么第一步生成的对象会被泄漏掉,因为没有在第3步被保存到 std::shared_ptr。
法一:使用std::make_shared可以避免这个问题。调用代码如下:

processWidget(std::make_shared<Widget>(),       
              computePriority());    // no potential resource leak

       在运行期,std::make_shared或者computePriority被首先调用。如果是std::make_shared被首先调用,指向动态内存对象的原始指针会被安全的保存在返回的std::shared_ptr对象中,然后是computePriority被调用 。如果computePriority产生了异常,那std::shared_ptr析构会知道于是它所拥有的对象会被销毁。如果computePriority先调用并产生了异常,std::make_shared不会被调用,因此也不会有动态分配的内存担心。

(注:假如我们把std::shared_ptr和std::make_shared替换成std::unique_ptr 和std::make_unique,会发生相同的事情。使用std::make_unique来代替new在写异常安全的代码里是和使用std::make_shared一样重要)

法二:确保你直接用new的时候,立即把new的结果传递给一个灵巧指针的构造函数,别的什么先不做,避免在new和灵巧指针的构造函数(会接管new出来的对象)之间产生异常。

程序示例:
std::shared_ptr<Widget> spw(new Widget, cusDel);
processWidget(spw, computePriority()); // correct, but not optimal; see below

对比非异常安全和异常安全代码的不同之处,我们在非异常安全的代码里给processWidget传递了一个右值。
processWidget(std::shared_ptr<Widget>(new Widget), computePriority());// arg is rvalue

而在异常安全的调用中,我们传递了一个左值
processWidget(spw, computePriority()); // arg is lvalue

//优化的做法
    因为processWidget的std::shared_ptr参数是通过传值的,从一个右值去构造仅仅需要一个move,而从左值去构造需要一个拷贝。
对std::shared_ptr来说,这个区别很重要,因为拷贝一个std::shared_ptr需要对其引用计数进行加1的原子操作,而移动一个
std::shared_ptr根本不需要对引用计数进行操作。对于这段异常安全的代码如果要达到非异常安全的代码的性能,我们在spw上应用
std::move,而把它转化成一个右值(见条款23):
processWidget(std::move(spw),computePriority()); // both efficient andexception safe

示例代码说明:因为processWidget的std::shared_ptr参数是值传递的,从一个右值去构造仅仅需要一个move,而从左值去构造需要一个拷贝。对std::shared_ptr来说,这个区别很重要,因为拷贝一个std::shared_ptr需要对其引用计数进行加1的原子操作,而移动一个std::shared_ptr根本不需要对引用计数进行操作。对于这段异常安全的代码如果要达到非异常安全的代码的性能,我们在spw上应用std::move,而把它转化成一个右值(见条款23) 

③第三个使用std::make_shared的优势(和直接使用new相比)是会提升性能。使用std::make_shared会让编译器产生更小更快的代码,从而产生更简洁的数据结构。

考虑如下直接使用new的代码:
std::shared_ptr<Widget> spw(new Widget);

       很显然这段代码会引起一个内存分配,但实际上是有两次内存分配。条款19解释了每个std::shared_ptr会指向一个控制块,这个控制块除了其他一些东西,包含了所指对象的引用计数。控制块的内存分配是std::shared_ptr的构造函数进行的。这样直接用new需要为Widget来分配一次内存,还要为控制块再分配一次内存。 

假如用std::make_shared来代替new,
auto spw = std::make_shared<Widget>();

       这样一次内存分配就足够了。那是因为std::make_shared会分配一块独立的内存既保存Widget对象又保存控制块。这个优化减小了程序的静态尺寸,因为代码只包含一次内存分配的调用,同时增加了代码执行速度,因为只有一次内存分配。

(注:对std::make_shared的性能分析同样适用于std::allocated_shared,因此std::make_shared的性能优势也同样存在于std::allocated_shared)

<3>make函数的使用限制场景
         make函数的参数相对直接使用new来说也更健壮。尽管有如此多的工程特性、异常安全以及效率优势,我们这个条款是“尽量”使用make函数,而没有说排除其他情况。那是因为还有情况不能或者不应该使用make函数:

(一)make函数都不允许使用定制删除器(见条款18,条款19),但是std::unique_ptr和std::shared_ptr的构造函数都可以给Widget对象一个定制删除器。

auto widgetDeleter = [](Widget* pw) { … };

直接使用new来构造一个有定制删除器的灵巧指针:
std::unique_ptr<Widget, decltype(widgetDeleter)> upw(new Widget, widgetDeleter);
std::shared_ptr<Widget> spw(new Widget, widgetDeleter);
用make函数没法做到这一点。

(二)make函数 中大括号初始化器不能够完美传递。   

        make函数的第二个限制是无法从实现中获得句法细节。条款7解释了当创建一个对象时,如果其类型通过std::initializer_list参数列表来重载构造函数的,尽量用大括号来创建对象而不是std::initializer_list构造函数。相反,用圆括号创建对象时,会调用non-std::initializer_list构造函数make函数完美传递了参数列表到对象的构造函数。但它们在使用圆括号或大括号时,并不能完美传递。如下:

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

       结果指针是指向一个10个元素的数组每个元素值是20,还是指向2个元素的数组其值分别是10和20 ?或者无限制?
解析:好消息是并非无限制的 :两个调用都是构造了10元素的数组,每个元素值都是20。说明在make函数里,转移参数的代码使用了圆括号,而不是大括号。坏消息是,假如你想使用大括号初始化器( braced initializer)来创建自己的指向对象的指针,你必须直接使用new。使用make函数需要能够完美传递一个大括号初始化器的能力,但是,如条款30中所说的,大括号初始化器不能够完美传递

       但条款30也给出了一个补救方案:从大括号初始化器根据auto类型推导来创建一个 std::initializer_list对象,然后把auto对象传递给make函数:

// create std::initializer_list
auto initList = { 10, 20 };
// create std::vector using std::initializer_list ctor
auto spv = std::make_shared<std::vector<int>>(initList);

         对于std::unique_ptr来说,其make函数就只在这两种场景(定制删除器和大括号初始化器)有问题。对于std::shared_pr来说,其make函数的问题会更多一些:

(三) 一些类会定义自己的opeator new和operator delete。这表示全局的内存分配和释放函数对该对象不合适。通常情况下,类特定的这两个函数被设计成精确的分配或释放类大小的内存块,比如,类Widget的operator new和operator delete仅仅处理sizeof(Widget)大小的内存块。这两个函数作为定制的分配器(通过std::allocate_shared)和解析器(通过定制解析器),对std::shared_ptr的支持并不是很好的选择。因为std::allocate_shared需要的内存数量并不是动态分配的对象的大小,而是对象的大小加上控制块的大小。因此,对于某些对象,其类有特定的operate new和operator delete,使用make函数去创建并不是很好的选择。

(四)std::make_shared在尺寸和速度上的优点同直接使用new相比,阻止了std::shared_ptr的控制块作为管理对象单独进行一次内存分配。  当对象的引用计数变为0,对象被销毁(析构函数被调)。然而,直到控制块同样也被销毁,它所拥有的内存才被释放,因为两者都在同一块动态分配的内存上

        我前面提到过,控制块除了引用计数本身还包含了其他一些信息。引用计数记录了有多少std::shared_ptr指针指向控制块。另外控制块中还包含了第二个引用计数,记录了有多少个std::weak_ptr指针指向控制块。这第二个引用计数被称作weak count。当一个std::weak_ptr检查是否过期时(见条款19),它会检查控制块里的引用计数(并不是weak count)。假如引用计数为0(假如被指对象没有std::shared_ptr指向了从而已经被销毁),则过期,否则就没过期。
        只要有std::weak_ptr指向一个控制块(weak count大于0),那控制块就一定存在。只要控制块存在,包含它的内存必定存在。这样通过std::shared_ptr的make函数分配的函数则在最后一个std::shared_ptr和最后一个std::weak_ptr被销毁前不能被释放。
       假如对象类型很大,以至于最后一个std::shared_ptr和最后一个std::weak_ptr的销毁之间的时间不能忽略时,对象的销毁和内存的释放间会有个延迟发生。

小结:
1.同直接使用new相比,make函数减小了代码重复,提高了异常安全,并且对于std::make_shared和std::allcoated_shared,生成的代码会更小更快。
2.不能使用make函数的情况包括我们需要定制删除器和期望直接传递大括号初始化器。
3.对于std::shared_ptr,额外的不建议使用make函数的情况包括:
  (1)定制内存管理的类,
  (2)关注内存的系统,非常大的对象,以及生存期比 std::shared_ptr长的std::weak_ptr。

///参考 https://blog.csdn.net/p942005405/article/details/84635673

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值