相关文章推荐
耍酷的胡萝卜  ·  python ...·  1 周前    · 
豁达的椰子  ·  MySQL中datetime、date和ti ...·  1 年前    · 

原标题:比起直接使用new,更偏爱使用std::make_unique和std::make_shared

让我们从 std::make_unique std::make_shared 之间的比较开始讲起吧。 std::make_shared 是C++11的一部分,可惜的是, std::make_unique 不是,它在C++14才纳入标准库。如果你使用的是C++11,不用忧伤,因为 std::make_unique 的简单版本很容易写出来,不信你看:

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出来的原生指针构造std::unique_ptr,最后返回创建的std::unique_ptr。这种形式的函数不支持数组和自定义删除器,但它说明了只要一点点工作,你就可以创造你需要的make_unique了。你要记住不要把你自己的版本放入命名空间std,因为当你提升到C++14标准库实现的时候,你不会想要它和标准库的版本冲突。

std::make_uniquestd::make_shared是三个make函数中的其中两个,而make函数是:把任意集合的参数完美转发给动态分配对象的构造函数,然后返回一个指向那对象的智能指针。第三个make函数是std::allocate_shared,它的行为与std::make_shared类似,除了它第一个参数是个分配器,指定动态分配对象的方式。

通过琐碎比较使用make函数和不使用make函数创建智能指针,揭露了使用make函数更可取的第一个原因。考虑以下:

auto upw1(std::make_unique<Widget>());     // 使用make函数
std::unique_ptr<Widget> upw2(new Widget);  // 不使用make函数
auto spw1(std::make_shared<Widget>());     // 使用make函数
std::shared_ptr<Widget> spw2(new Widget);  // 不使用make函数

它们本质上的不同是:使用new的版本重复着需要创建的类型(即出现了两次Widget),而使用make函数不需要。重复出现类型和软件工程的关键原则产生冲突:应该避免代码重复。源码中重复的代码会增加编译时间,导致对象代码膨胀,并且通常会让代码库更难运行——这经常引发不合逻辑的代码,而不合逻辑的代码库一般会出现bug。除非是写两次比写一个更有效果,不然谁不喜欢少些点代码吗?

更偏爱使用make函数的第二个原因异常安全。假如我们有个函数,根据优先级来处理Widget:

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

现在呢,假如我们有个计算优先级的函数,

int computePriority();

然后我们用它和new创建的智能指针作为参数调用processWidget:

processWidget(std::shared_ptr<Widget>(new Widget), 
              computePriority());    // 可能会资源泄漏

就如注释所说,这代码中new出来的Widget可能会泄漏,但是为什么?std::shared_ptr是为了防止资源泄漏而设计的,当最后一个指向资源的std::shared_ptr对象消失,它们指向的资源也会被销毁。如果每个人无论什么地方都使用std::shared_ptr,C++还有内存泄漏这回事吗?

答案是在编译期间,源代码转换为目标码时(*.o文件)。在运行时间,函数的参数在函数运行前必须被求值,所以调用processWidget时,下面的事请会在processWidget开始前执行:

  • 表达式“new Widget”会被求值,即,一个Widget对象必须在堆上被创建。
  • std::shared_ptr的接收原生指针的构造函数一定要执行。
  • computePriority一定要运行。

编译器在生成代码时不会保证上面的执行顺序,“new Widget”一定会在std::shared_ptr构造函数之前执行,因为构造函数需要new的结果,但是computePriority可能在它们之前就被调用了,可能在它们之后,可能在它们之间。所以,编译器生成代码的执行顺序有可能是这样的:

  1. 执行“new Widget”。
  2. 执行computePriority。
  3. 执行std::shared_ptr的构造函数。

如果生成的代码真的是这样,那么在运行时,computePriority产生了异常,步骤1中动态分配的Widget就泄漏了,因为它没有被步骤3中的std::shared_ptr保存。

使用std::make_shared_ptr可以避免这问题。这样调用代码:

processWidget(std::make_shared<Widget>(), computePriority())

在运行期间,std::make_sharedcomputePriority都有可能先被调用,如果先调用的是std::make_shared,那么指向动态分配Widget对象的原生指针会安全地存储在要返回的std::shared_ptr中,然后再调用computePriority。如果computePriority产出异常,std::shared_ptr的析构函数就会销毁持有的Widget。而如果先调用的是computePriority,并且产生异常,std::make_shared就不会被执行,因此没有动态分配的Widget对象让你担心。

如果我们把std::shared_ptrstd::make_shared替换成std::unique_ptrstd::make_unique,效果一样。使用std::make_unique替代new的重要性就像使用std::make_shared那样:写异常安全的代码。

std::make_shared的一个特点(相比于直接使用new)是提高效率。使用std::make_shared允许编译器生成更小、更快的代码。考虑当我们直接使用new时:

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

很明显这代码涉及一次内存分配,不过,它实际上分配两次。每个std::shared_ptr内都含有一个指向控制块的指针,这控制块的内存是由std::shared_ptr的构造函数分配的,那么直接使用new,需要为Widget分配一次内存,还需要为控制块分配一次内存。

如果用std::make_shared呢,

auto spw = std::make_shared<Widget>();

一次分配就够了,因为std::make_shared会分配一大块内存来同时持有Widget对象和控制块。这种优化减少了程序的静态尺寸,因为代码只需要调用一次内存分配函数,然后它增加了代码执行的速度,因为只需要分配一次内存(说明是分配内存这个函数开销略大)。而且,使用std::make_shared能避免了一些控制块的簿记信息,潜在地减少了程序占用的内存空间。

std::allocate_shared的性能分析和std::make_shared一样,所以std::make_shared的性能优势也可以延伸到std::allocate_shared

比起直接使用new,更偏爱使用make函数,这个争论是很热烈的。虽有软件工程、异常安全、性能优势,不过,本条款的指导方针是更偏爱使用make函数,而不是单独依赖它们,这是因为在某些状况下它们不适用。

例如,没有一个make函数可以指定自定义删除器,但是std::unique_ptrstd::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函数的第二个限制是来源于它们实现的句法细节。当创建一个对象时,如果该对象的重载构造函数带有std::initializer_list参数,那么使用大括号创建对象会偏向于使用带std::initializer_list构造,要使用圆括号创建对象才能使用到非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的std::vector呢,还是指向两个元素、一个10、一个20的std::vector呢?还是说结果不能确定吗?

好消息是结果是能确定的:上面两个都创建内含10个值为20的std::vector。那意味着在make函数内,完美转发使用的是圆括号,而不是大括号。坏消息是如果你想用大括号初始化来构造指向的对象,你只能直接使用new,如果你想使用make函数,就要求完美转发的能力支持大括号初始化,但是大括号初始化不能被完美转发。不过也有一种能工作的方法:用auto推断大括号,从而创建一个std::initializer_list对象,然后把auto变量传递给make函数:

// 创建 std::initializer_list
auto initList = {10, 20};
// 使用std::initializer_list构造函数创建std::vector,容器中只有两个元素
auto spv = std::make_shared<std::vector<int>>(initList);

对于std::unique_ptr,只有两种情况(自定义删除器和大括号初始化)会让它的make函数出问题。对于std::shared_ptr和它的make函数,就多两种情况,这两种情况都是边缘情况,不过一些开发者就喜欢住在边缘,你可能就是他们中第一个。

一些类定义了自己的operator newoperator delete函数,这些函数的出现暗示着常规的全局内存分配和回收不适合这种类型的对象。通常情况下,设计这些函数只有为了精确分配和销毁对象,例如,Widget对象的operator newoperator delete只有为了精确分配和回收大小为sizeof(Widget)的内存块才会设计。这两个函数不适合std::shared_ptr的自定义分配(借助std::allocate_shared)和回收(借助自定义删除器),因为std::allocate_shared请求内存的大小不是对象的尺寸,而是对象尺寸加上控制块尺寸。结果就是,使用make函数为那些——定义自己版本的operator newoperator delete的——类创建对象是个糟糕的想法。

比起直接使用newstd::make_shared的占用内存大小和速度优势来源于:std::shared_ptr的控制块与它管理的对象放在同一块内存。当引用计数为0时,对象被销毁(即调用了析构函数),但是,它使用的内存不会释放,除非控制块也被销毁,因为对象和控制块在同一块动态分配的内存上。

就像我提起那样,控制块上除了引用计数还有别的薄记信息。引用计数记录的是有多少std::shared_ptr指向控制块,但是控制块还有第二种引用计数,记录有多少std::weak_ptr指向控制块。这种引用计数称为weak count。当std::weak_ptr检查它是否过期时(expired),它通过检查控制块中的引用计数(不是weak count)来实现。如果引用计数为0(即没有std::shared_ptr指向这个对象,因此被销毁),std::weak_ptr就过期,否则就没有过期。

但是,只要有std::weak_ptr指向控制块(weak count大于0),控制块就必须继续存在,而只要控制块存在,容纳它的内存块也依旧存在。那么,通过make函数创建对象分配的内存,要直到最后一个指向它的std::shared_ptrstd::weak_ptr对象销毁,才能被回收。

如果对象的类型非常大,并且最后一个std::shared_ptr销毁和最后一个std::weak_ptr销毁之间的时间间隔很大,那么是对象销毁和内存被回收之间的会有延迟:

class ReallyBigType { ... };
auto pBigObj =                          // 借助std::make_shared
   std::make_shared<ReallyBigType>();   // 创建类型非常大的对象
...               // 创建std::shared_ptr和std::weak_ptr指向对象 
...               // 最后一个std::shared_ptr被销毁,那仍有std::weak_ptr存在
...               // 在这个期间,之前类型非常大的对象使用的内存仍然被占用
...               // 最后一个std::weak被销毁,控制块和对象共占的内存被释放

如果直接使用new,ReallyBigType对象的内存只要在最后一个std::shared_ptr被销毁就能被释放:

class ReallyBigType { ... };
auto pBigObj =                          // 借助std::make_shared
   std::make_shared<ReallyBigType>();   // 创建类型非常大的对象
...               // 创建std::shared_ptr和std::weak_ptr指向对象 
...               // 最后一个std::shared_ptr被销毁,那仍有std::weak_ptr存在
...               // 在这个期间,之前类型非常大的对象使用的内存仍然被占用
...               // 最后一个std::weak被销毁,控制块和对象共占的内存被释放

当你发现某些情况不能使用或者不适合使用std::make_shared,却又想要防止容易发生的异常安全问题。最好的办法就是确保当你直接使用new时,用一条语句执行——把new的结果马上传递给智能指针的构造函数,并且该语句就做这一件事。这防止编译器生成newstd::shared_ptr构造之间发出异常。

作为例子,我们修改之前的异常不安全processWidget,并指定自定义删除器:

void processWidget(std::shared_ptr<Widget> spw, int priority); // 如前
void cusDel(Widget *ptr);      //  自定义删除器

这里是异常不安全的调用:

processWidget(           // 如前,可能资源泄漏
   std::shared_ptr<Widget>(new Widget, cusDel),
   computePriority()

回忆:如果computePriority调用在“new Widget”之前,std::shared_ptr构造之后,然后computePriority产生异常,那么动态分配的Widget就会泄漏。

这里要使用自定义删除器,不能使用std::make_shared,所以避免泄漏的方法就是把分配Widget和std::shared_ptr构造放在只属于它们的语句,然后再用std::shared_ptr的结果调用processWidget。这是这项技术的本质部分,等下我们可见到,我们可以修改它从而提高性能:

std::shared_ptr<Widget> spw(new Widget, cusDel);
processWidget(spw, computeWidget);  // 正确,但没有优化,看下面

这代码是可行的,因为std::shared_ptr得到了原生指针的所有权,尽管构造函数可能发出异常。在这个例子中,如果spw的构造期间抛出异常(例如,由于不能为控制块动态分配内存),也能保证cusDel被调用(以“new Widget”的结果为参数)。

有个小小的性能问题,在异常不安全的调用中,我们传给processWidget的是一个右值,

processWidget(
   std::shared_ptr<Widget>(new Widget, cusDel),  // 参数是右值
   computePriority()

但是在异常安全的调用中,我们传递的是个左值:

processWidget(spw, computePriority());   // 参数是左值

因为processWidget的std::shared_ptr参数是值传递,从一个右值构造使用的是移动,从一个左值构造使用的是拷贝。对于std::shared_ptr,这差别挺大的,因为拷贝一个std::shared_ptr需要增加它的引用计数,这是原子操作,而移动操作完全不用操作引用计数。针对于异常安全代码想要达到异常不安全代码的性能水平,我们需要使用std::move来把spw转化为右值:

processWidget(std::move(spw), computePriority());  // 现在也一样高效

这是有趣的而且值得知道,但是通常也是不相干的,因为你很少有理由不用make函数,除非你有迫不得已的理由,否则,你应该使用make函数。

需要记住的3点:

  • 相比于直接使用new,make函数可以消除代码重复,提高异常安全,而且std::make_sharedstd::allocate_shared生成的代码更小更快。
  • 不适合使用make函数的场合包括需要指定自定义删除器和想要传递大括号初始值。
  • 对于std::shared_ptr,使用make函数可能是不明智的额外场合包括(1)自定义内存管理函数的类和(2)内存紧张的系统中,有非常大的对象,然后std::weak_ptrstd::shared_ptr长寿。
原文链接: https://blog.csdn.net/big_yellow_duck/article/details/52347700 相比于直接使用new,make函数可以消除代码重复,提高异常安全,而且和生成的代码更小更快。不适合使用make函数的场合包括需要指定自定义删除器和想要传递大括号初始值。对于,使用make函数可能是不明智的额外场合包括(1)自定义内存管理函数的类和(2)内存紧张的系统中,有非常大的对象,然后比长寿。
std::tuple是C++ 11中引入的一个非常有用的结构,以前我们要返回一个包含不同数据类型的返回值,一般都需要自定义一个结构体或者通过函数的参数来返回,现在std::tuple就可以帮我们搞定。 1.引用头文件 #include 2. Tuple初始化 std::tuple的初始化可以通过构造函数实现。 // Creating and Initializing a tuple std::tuple result1 { 22, 19.28, "text" }; 这种初始化方式要定义各个元素的数据类型,比较繁琐,
shared_ptr<string> p1 = make_shared<string>(10, '9'); shared_ptr<string> p2 = make_shared<string>("hello"); shared_ptr<string&...
一、c++11中的智能指针数组创建 在c++11中引入了智能指针这个概念,这个非常好,但是有一个问题显然被忘记了,如何动态创建智能指针数组,在c++11中没有提供直接的函数。换句话说,创建智能指针的make_shared,不支持创建数组。那在c++11中如何创建一个智能指针数组呢?只能自己封装或者变通实现,在c++14后可以支持构造函数创建智能指针数组,可这仍然不太符合技术规范发展的一致性,可继承性。 二、c++20中的智能指针数组的创建 基于上述的情况,在c++20中使用std::make_shared
C++中,动态内存的管理是通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针,可以选择对对象进行初始化;delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。 动态内存的使用很容易出问题,因为确保在正确的时间释放内存是极其困难的。有时会忘记释放内存,在这种情况下会产生内存泄露;有时在尚有指针引用内存的情况下就释放了它,在这种情况下就会产生引用...
std::make_shared make_shared函数的主要功能是在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr;由于是通过shared_ptr管理内存,因此一种安全分配和使用动态内存的方法。 如下为make_shared的使用: //p1指向一个值为"9999999999"的string shared_ptr<string> p1 = make_shared<string>(10, '9'); shared_ptr<string> p.
C++11 中引入了智能指针, 同时还有一个模板函数 std::make_shared 可以返回一个指定类型的 std::shared_ptr, 那与 std::shared_ptr 的构造函数相比它能给我们带来什么好处呢 ? shared_ptr 需要维护引用计数的信息, 强引用, 用来记录当前有多少个存活的 shared_ptrs 正持有该对象. 共享的对象会在最后一个...
// read poppler config file globalParams = std::make_unique<GlobalParams>( !param.poppler_data_dir.empty() ? param.poppler_data_dir.c_str() : NULL 找了下,英文解释: template< class T, class... Args > unique_ptr&
`std::make_unique`和`std::make_shared`都是C++11引入的内存管理工具,用于创建动态对象并返回其指针。 `std::make_unique`用于创建独占所有权的对象,即该对象的所有权只能被一个`std::unique_ptr`持有。它的语法如下: template< class T, class... Args > std::unique_ptr<T> make_unique( Args&&... args ); 其中,`T`是要创建的对象类型,`Args`是传递给对象构造函数的参数列表。`make_unique`函数返回一个`std::unique_ptr<T>`对象,该对象拥有对新创建的`T`对象的所有权。 `std::make_shared`用于创建共享所有权的对象,即该对象可以被多个`std::shared_ptr`共享。它的语法如下: template< class T, class... Args > std::shared_ptr<T> make_shared( Args&&... args ); 其中,`T`是要创建的对象类型,`Args`是传递给对象构造函数的参数列表。`make_shared`函数返回一个`std::shared_ptr<T>`对象,该对象拥有对新创建的`T`对象的共享所有权。 值得注意的是,`std::make_shared`相较于直接使用`std::shared_ptr<T>`创建对象,具有更好的性能和更少的内存分配次数。因此,在需要共享对象所有权时,应优先使用`std::make_shared`。