元芳你怎么看

本网站主要用于记录我个人学习的内容,希望对你有所帮助

0%

条款 21 :优先选用 std::make_unique 和std::make_shared, 而非直接使用 new

使用std::make_shared的理由:代码精简

1
2
3
4
auto upw1(std::make_unique<Widget>());
std::unique_ptr<Widget> upw2(new Widget);
auto spw1(std::make_shared<Widget>());
std::shared_ptr<Widget> spw2(new Widget);

使用make系列函数只有一个Widget,代码风格不冗余。

使用std::make_shared的理由:异常安全性

1
2
3
4
5
6
class MyClass{};
void test1(std::shared_ptr<MyClass> share_p , int count);
int test2();
int main(){
test1(std::shared_ptr<MyClass>(new MyClass),test2());
}

对于上面代码可能会造成内存泄漏.这和编译器从源代码到目标代码的翻译有关系
理想情况test1()运行前发生:

  1. “new MyClass”完成评估求值(在堆创建)
  2. new 出来的裸指针被shared_ptr托管(运行shared_ptr的构造函数)
  3. 运行test2()
    而编译器不必按照上述顺序生成代码.当然”new MyClass”必须在shared_ptr的构造函数调用前执行完,但是test2()并没有强制它的运行顺序,也就是说test2()可能在1和2的调用前,调用后,甚至1和2调用之间执行.你可能觉得无伤大雅,但是如果test2()在运行时产生了异常呢?
    此时第一步动态分配的MyClass会被泄漏.
如何解决这个问题呢?
分开写

没错,编译器分不清顺序,你就让它分清就好了,将单独的语句改写成多行

1
2
std::shared_ptr<MyClass> spw(new MyClass,MyDel);//MyDel是自定义析构器
test1(std::move(spw),test2()); //移动可以提高效率,并且防止复制对引用计数进行原子的递增操
使用std::make_shared
1
test1(std::make_shared<MyClass>(),test2());

这里如果test2()先运行且异常,动态还没分配MyClass
如果先运行std::make_shared(),动态分配的裸指针会安全的存储在std::shared_ptr对象中,之后test2()异常也能正常析构
.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
2
3
4
auto MyDel = [](MyClass* pw) {...};
std::unique_ptr<MyClass, decltype(MyDel)> upw(new MyClass, MyDel);
std::shared_ptr<MyClass> spw(new MyClass, MyDel); // MyDel是自定义析构器
test1(std::move(spw),test2()); // 移动可以提高效率,并且防止复制对引用计数进行原子的递增操
不能完美转发大括号初始化物
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
int test1() {

auto upv =
std::make_unique<std::vector<int>>(std::initializer_list<int>{10, 20});

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

std::cout << "std::unique_ptr elements: ";
for (const auto& element : *upv) {
std::cout << element << " ";
}
std::cout << std::endl;

std::cout << "std::shared_ptr elements: ";
for (const auto& element : *spv) {
std::cout << element << " ";
}
std::cout << std::endl;

return 0;
}

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

std::cout << "std::unique_ptr elements: ";
for (const auto& element : *upv) {
std::cout << element << " ";
}
std::cout << std::endl;

std::cout << "std::shared_ptr elements: ";
for (const auto& element : *spv) {
std::cout << element << " ";
}
std::cout << std::endl;

return 0;
}

依次运行test1()和test2()得到的结果是:

1
2
3
4
5
➜  tmp git:(main) ✗ ./tmp
std::unique_ptr elements: 10 20
std::shared_ptr elements: 10 20
std::unique_ptr elements: 20 20 20 20 20 20 20 20 20 20
std::shared_ptr elements: 20 20 20 20 20 20 20 20 20 20
1
2
auto initlist = {10, 20};
auto spv = std::make_shared<std::vector<int>>(initlist);

对于test1()中upv和spv的初始化,我更希望能使用第二种,没有人会想写第一种那么复杂的代码的.示例代码仅仅展示auto推导出来的类型.