使用std::make_shared的理由:代码精简
1 | auto upw1(std::make_unique<Widget>()); |
使用make系列函数只有一个Widget,代码风格不冗余。
使用std::make_shared的理由:异常安全性
1 | class MyClass{}; |
对于上面代码可能会造成内存泄漏.这和编译器从源代码到目标代码的翻译有关系
理想情况test1()运行前发生:
- “new MyClass”完成评估求值(在堆创建)
- new 出来的裸指针被shared_ptr托管(运行shared_ptr的构造函数)
- 运行test2()
而编译器不必按照上述顺序生成代码.当然”new MyClass”必须在shared_ptr的构造函数调用前执行完,但是test2()并没有强制它的运行顺序,也就是说test2()可能在1和2的调用前,调用后,甚至1和2调用之间执行.你可能觉得无伤大雅,但是如果test2()在运行时产生了异常呢?
此时第一步动态分配的MyClass会被泄漏.
如何解决这个问题呢?
分开写
没错,编译器分不清顺序,你就让它分清就好了,将单独的语句改写成多行
1 | std::shared_ptr<MyClass> spw(new MyClass,MyDel);//MyDel是自定义析构器 |
使用std::make_shared
1 | test1(std::make_shared<MyClass>(),test2()); |
这里如果test2()先运行且异常,动态还没分配MyClass
如果先运行std::make_shared
.std::unique_ptr和std::make_unique同理
使用std::make_shared的另一个理由:性能提升
shared_ptr 直接使用 new 表达式的话,除了要为 MyClass 进行一次内存分配,还要为与其相关联的控制块再进行一次内存分配.
而使用make_shared来代替new表达式的话,仅仅需要一次内存分配.
make系列函数也不是万能的
说了这么多,make系列的函数无论是在异常安全,效率,避免代码冗余都有优势,但是不能排他性的只用make系列函数,因为有些情景下不能使用make系列函数.比如,自定义析构器,无法完美转发大括号初始化物,定义自身版本的operator new和operator delete(shared_ptr).
对于operator new和operator delete这种边缘情况就不做解释,知道有这么个东西就可以了.
自定义析构器
所有的make系列函数不允许使用自定义析构器. std::unique_ptr 和 std::shared_ptr 却都有着允许使用自定义析构器的构造函数:
1 | auto MyDel = [](MyClass* pw) {...}; |
不能完美转发大括号初始化物
1 | auto upv = std::make_unique<std::vector<int>>(10, 20); |
最终upv指涉到的是一个包含10个元素,每个元素值都为20的vector?还是包含两个元素,分别是10,20的vector?但是不能完美转发大括号初始化物该怎么办?
答案是make系列的函数对形参进行完美转发的代码使用的是圆括号而非大括号.所以是一个包含10个元素,每个元素值都为20的vector.也不是不能完美转发,变通以下还是可以的.比如下面的test1()创建一个std::initializer_list对象,并且利用std::initializer_list型别的构造函数构造vector.
1 | int test1() { |
依次运行test1()和test2()得到的结果是:
1 | ➜ tmp git:(main) ✗ ./tmp |
1 | auto initlist = {10, 20}; |
对于test1()中upv和spv的初始化,我更希望能使用第二种,没有人会想写第一种那么复杂的代码的.示例代码仅仅展示auto推导出来的类型.