这里我们把std::make_unique
和std::make_shared
放到同一起跑线作为本条款的开端。如果您使用的是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>(prams)...));
}
如上述代码可知,make_unique仅仅是将其形参向待创建对象构造函数做了一次完美转发,从一个new的运算符产生的裸指针出发,构造了一个std::unique_ptr
而已。这个形式的函数不支持数组和自定义析构器,但它证明了只需通过一点点的努力就可以根据需要创建一个make_unique。但需要记住的是,不要把自己版本的make_unique放入std,否则等代码升级编译器的时候就报错了。
和std::make_shared
与std::make_unique
并列的,其实还有个std::allocate_shared
。这个函数的行为和std::make_shared
一样,只不过它的第一个参数是用以动态分配内存的分配器对象。
使用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系列函数
上述不使用make系列的函数,会产生重复撰写的问题。而代码冗余应该避免,因为冗余很容易导致不一致的缺陷。
使用make系列函数的优势–确保异常安全
假设这样一个场景:
void processWidget(std::shared_ptr<Widget> spw, int priortiy);
假设现在有一个函数来计算优先级:
int computePriority();
那么我们可能会有这样的代码:
processWidget(std::shared_ptr<Widget>(new Widget), computePrority()); //这里存在潜在的资源泄露风险
原因在哪呢?是与编译器从源代码翻译到目标代码的情况有关。在运行期,传递给函数的实参必须在函数调用被发起之前完成求值。所以在processWidget
运行之前,会先执行如下步骤:
-
表达式
new Wdiget
必须先进行求值。即一个Widget对象必须现在堆上创建。 -
由new产生的裸指针的托管对象
std::shared_ptr<Widget>
的构造函数必须执行。 -
computePriority必须运行。
而上述三个步骤并不一定会按照某种固定顺序执行,唯一的区别只是第一条必须在第二条之前完成,其他的顺序要求没有。那么就有可能出现这样的顺序:
-
实施“new Widget”。
-
执行computePriority。
-
运行std::shared_ptr构造函数。
如果第2步出现异常,那么第一步动态分配的Widget会被泄露。因为它将永远不会被存储到第3步才接管的std::shared_ptr中。但是使用std::make_shared
就可以避免该问题。
processWidget(std::make_shared<Widget>(), //不会发生潜在的资源泄露风险
computePriority());
按照以上写法,std::make_shared
和computePriority()
肯定有一个先调用。
-
std::make_shared
先调用:则动态分配裸指针会在computePriority()
异常之前保存在std::shared_ptr
对象中。computePriority()
异常之后,也能保证std::shared_ptr
的析构函数正确释放资源。 -
如果
computePriority()
先调用:那么直接异常也不会造成动态分配了。
这种逻辑在std::make_shared
和std::make_unique
的时候判断一样,用make系列函数可以保证异常安全。
使用make系列函数的优势–提高性能
使用make系列函数会让编译器有机会利用更简洁的数据结构产生更小更快的代码。
std::shared_ptr<Widget> spw(new Widget);
显然这段代码会引发一次内存分配,但实际上会引发两次。Item 19解释过,每个std::shared_ptr
会指涉到一个控制块,除了其他东西之外,该控制块包含了所指涉到的对象相关联的计数引用。控制块的内存是std::shared_ptr
的构造函数进行分配的。因此,直接使用new表达式的话,除了要为Widget进行一次内存分配,还要为控制块分配一次。
但如果使用下面代码:
auto spw = std::make_shared<Widget>();
一次内存分配足矣。因为std::make_shared
会一次分配好单块内存同时可以放置对象和对应的控制块。
这种优化:
-
减小了程序的静态尺寸,因为代码只包含一次内存分配调用
-
增加了可执行代码的运行速度,因为内存是一次性分配出来的
-
std::make_shared
还能避免控制块中一些信息的必要性,能够潜在减少了程序的内存痕迹总量
同理,std::make_shared
的性能分析也适用于std::allocate_shared
。
特例情况无法使用std::make_shared
和std::make_unique
-
所有的make系列函数不允许使用自定义析构器。
-
对于有初始化列表构造函数的对象,make系列函数会抑制初始化列表构造调用,因为底层是用的圆括号而不是大括号构造的。如果非要使用大括号构造函数也不想放弃
std::make_shared
,则要这么写。
auto initList = {10, 20};
auto spv = std::make_shared<std::vector<int>>(initList);
特例情况无法使用std::make_shared
对于std::make_shared
还有两种额外情况无法使用:
有些类会定义自身版本的operator new和operator delete。
这些函数意味着全局版本的内存分配和释放不适用于这个对象。通常情况下,类自定义的这两种函数被设计成仅用来分配和释放该类精确尺寸的内存块。而这样的分配器对于对象+控制块的内存大小显然不满足,所以通常不要使用make系列函数去为带有自定义版本的operator new和operator delete的类创建对象。
双刃剑std::make_shared
的劣势
之前说了std::make_shared
效率高体现在一块内存上的单块分配。但这样也带来一个弊端,析构的时候,也需要等两者均无人引用时,才可一起释放。简单的说,拖慢了内存块释放的速度。
这种弊端常见于使用了std::weak_ptr
的场景。在std::shared_ptr
对象都没人引用,但是std::weak_ptr
还在引用时,对应的对象空间本可以释放,只留控制块空间即可。但是由于是一块内存的原因,导致对应对象也无法立即释放。也就是说,在std::make_shared
生成的场景下,内存块必须在最后一个std::shared_ptr
和std::weak_ptr
对象均析构的时候,才能得到释放。
在std::make_shared
劣势明显的时候,如何保证异常安全
考虑之前异常安全无保证的例子:
void processWidget(std::shared_ptr<Widget> spw,
int priority);
void cusDel(Widget *ptr);
processWidget( //潜在的资源泄露风险
std::shared_ptr<Widget>(new Widget, cusDel),
computePriority()
);
有了之前的分析,那么很容易写出如下代码:
std::shared_ptr<Widget> spw(new Widget, cusDel);
processWidget(spw, computePriority()); //正确但并非最优化
processWidget(std::move(spw), computePriority()); //正确且高效率
第二种写法使用移动语义,不会对引用计数进行变化,而变化引用计数是原子操作,开销较大。
但仍然要强调的是,平常你很少有机会不能使用std::make_shared
,除非你有充分的理由不能使用,否则都应该使用它们。
要点速记 |
---|
1. 相对于直接使用new比倒是,make系列函数消除了重复代码,改进了异常安全性,并且生成的目标代码尺寸更小,速度更快。 |
2. 不适用于make系列的场景包括,自定义删除器以及期望直接传递大括号初始物。 |
3. 对于std::shared_ptr ,不建议适用make系列寒素的场景包括:①自定义内存管理的类②内存紧张的系统,非常大的对象以及存在比指涉std::shared_ptr 对象生存期更长的std::weak_ptr 对象。 |