item19:complete

This commit is contained in:
kelthuzadx 2019-07-05 15:06:39 +08:00
parent 5dd5a9b171
commit 654bea9d6e
2 changed files with 101 additions and 1 deletions

View File

@ -54,3 +54,103 @@ std::vector<std::shared_ptr<Widget>> vpw{ pw1, pw2 };
+ **std::make_shared总是创建一个控制块**(参见Item21)。它创建一个指向新对象的指针,所以可以肯定此时没有其他`std::make_shared`的控制块被调用。
+ **当从独占指针上构造出`std::shared_ptr`时会创建控制块即std::unique_ptr或者std::auto_ptr**。独占指针没有使用控制块,所以指针指向的对象没有关联其他控制块。(作为构造的一部分,`std::shared_ptr`侵占独占指针指向对象的独占权,所以`std::unique_ptr`被设置为null
+ **当从原始指针上构造出`std::shared_ptr`时会创建控制块**。如果你想从一个早已存在控制块的对象上创建`std::shared_ptr`,你将假定传递一个`std::shared_ptr`或者`std::weak_ptr`作为构造函数实参,而不是原始指针。用`std::shared_ptr`或者`std::weak_ptr`作为构造函数实参创建`std::shared_ptr`不会创建新控制块,因为它可以依赖传递来的智能指针的控制块。
这些规则造成的后果就是从原始指针上构造超过一个`std::shared_ptr`就会让你走上未定义行为的快车道,因为指向的对象有多个控制块关联。多个控制块意味着多个引用计数值,多个引用计数值意味着对象将会被销毁多次(每个引用计数值一次)。那意味着下面的代码是有问题的,很有问题,问题很大:
```cpp
auto pw = new Widget; // pw是原始指针
std::shared_ptr<Widget> spw1(pw, loggingDel); // 为*pw创建控制块
std::shared_ptr<Widget> spw2(pw, loggingDel); // 为*pw创建第二个控制块
```
创建原始指针指向动态分配的对象很糟糕因为它完全背离了这章的建议对于共享资源使用std::shared_ptr而不是原始指针。如果你忘记了该建议的动机请翻到115页。撇开那个不说创建**pw**那一行代码虽然让人厌恶,但是至少不会造成未定义程序行为。
现在,传给**spw1**的构造函数一个原始指针,它会为指向的对象创建一个控制块(引用计数值在里面)。这种情况下,指向的对象是`*pw`。就其本身而言没什么问题,但是将同样的原始指针传递给**spw2**的构造函数会再次为`*pw`创建一个控制块。因此`*pw`有两个引用计数值,每一个最后都会变成零,然后最终导致销毁`*pw`两次。第二个销毁会导致未定义行为。
`std::shared_ptr`给我们上了两堂课。第一个,避免传给`std::shared_ptr`构造函数原始指针。通常替代方案是使用`std::make_shared`(参见Item21),但是上面例子中,我们使用自定义的销毁器,用`std::make_shared`就没办法做到。第二个,如果你必须传给`std::shared_ptr`构造函数原始指针直接传new出来的结果不要传指针变量。如果上面代码第一部分这样重写
```cpp
std::shared_ptr<Widget> spw1(new Widget, // 直接使用new的结果
loggingDel);
```
会少了很多创建第二个从原始指针上构造`std::shared_ptr`的诱惑。相应的创建spw2也会很自然的用spw1作为初始化参数即使用`std::shared_ptr`拷贝构造),那就没什么问题了:
```CPP
std::shared_ptr<Widget> spw2(spw1); // spw2使用spw1一样的控制块
```
一个尤其令人意外的地方是使用**this**原始指针作为`std::shared_ptr`构造函数实参的时候可能导致创建多个控制块。假设我们的程序使用`std::shared_ptr`管理**Widget**对象,我们有一个数据结构用于跟踪已经处理过的**Widget**对象:
```cpp
std::vector<std::shared_ptr<Widget>> processedWidgets;
```
继续,假设**Widget**有一个用于处理的成员函数:
```cpp
class Widget {
public:
void process();
};
```
对于**Widget::process**看起来合理的代码如下:
```cpp
void Widget::process()
{
… // 处理Widget
processedWidgets.emplace_back(this); // 然后将他加到已处理过的Widget的列表中
// 这是错的
}
```
评论已经说了这是错的——或者至少来说大部分是错的。错误的部分是传递this而不使用**emplace_back**)。如果你不熟悉**emplace_back**参见Item42。上面的代码可以通过编译但是向容器传递一个原始指针this`std::shared_ptr`会由此为指向的对象(`*this`)创建一个控制块。那看起来没什么问题,直到你意识到如果成员函数外面早已存在指向**Widget**对象的指针它是未定义行为的Game, Set, and Match译注一部电影但是译者没看过。。。
`std::shared_ptr`API已有处理这种情况的设施。它的名字可能是C++标准库中最奇怪的:`std::enable_shared_from_this`。它是一个用做基类的模板,模板类型参数是某个想被`std::shared_ptr`管理且能从该类型的**this**对象上安全创建`std::shared_ptr`指针的存在。在我们的例子中,**Widget**将会继承自`std::enable_shared_from_this`
```cpp
class Widget: public std::enable_shared_from_this<Widget> {
public:
void process();
};
```
正如我所说,`std::enable_shared_from_this`是一个基类类型模板。他的类型参数总是某个继承自它的类,所以**Widget**继承自`std::enable_shared_from_this<Widget>`。如果某类型继承自一个由该类型(译注:作为模板类型参数)进行模板化得到的基类这个东西让你心脏有点遭不住,别去想它就好了。代码完全合法,而且它背后的设计模式也是没问题的,并且这种设计模式还有个标准名字,尽管该名字和`std::enable_shared_from_this`一样怪异。这个标准名字就是奇异递归模板模式The Curiously Recurring Template Pattern(CRTP))。如果你想学更多关于它的内容,请放手一搜,现在我们要回到`std::enable_shared_from_this`上。
`std::enable_shared_from_this`定义了一个成员函数,成员函数会创建指向当前对象的`std::shared_ptr`却不创建多余控制块。这个成员函数就是`shared_from_this`,无论在哪当你想使用`std::shared_ptr`指向this所指对象时都请使用它。这里有个`Widget::process`的安全实现:
```cpp
void Widget::process()
{
// 和之前一样处理Widget
// 把指向当前对象的shared_ptr加入processedWidgets
processedWidgets.emplace_back(shared_from_this());
}
```
从内部来说,`shared_from_this`查找当前对象控制块,然后创建一个新的`std::shared_ptr`指向这个控制块。设计的依据是当前对象已经存在一个关联的控制块。如果是那种情况,必须已经存在一个指向当前对象的`std::shared_ptr`(即调用shared_from_this的成员函数外面已经存在一个`std::shared_ptr`)。如果没有`std::shared_ptr`指向当前对象即当前对象没有关联控制块行为是未定义的即使shared_from_this通常会跑出一个异常。
要想防止客户端在调用`std::shared_ptr`前先调用`shared_from_this`,继承自`std::enable_shared_from_this`的类通常将它们的构造函数声明为private并且让客户端通过工厂方法创建`std::enable_shared_from_this`对象。以**Widget**为例,代码可以是这样:
```cpp
class Widget: public std::enable_shared_from_this<Widget> {
public:
// 完美转发参数的工厂方法
template<typename... Ts>
static std::shared_ptr<Widget> create(Ts&&... params);
void process(); // 和前面一样
private:
};
```
现在,你可能隐约记得我们讨论控制块的动机是想了解`std::shared_ptr`关联一个控制块的成本。既然我们已经直到了怎么避免创建过多控制块,就让我们回到原来的主题。
控制块通常只占用几个word自定义的销毁器和分配器可能会让它变大一点。通常通知快的实现比你想的更复杂一些。它使用继承甚至里面还有一个虚函数用来确保指向的对象被正确销毁。这意味着使用`std::shared_ptr`还会招致控制块使用虚函数带来的成本。
读完了动态分配控制块,任意大小的销毁器和分配器,虚函数机制,原子引用计数修改,你对于`std::shared_ptr`的热情可能有点消退。可以理解,对每个资源管理问题来说都没有最佳解决方案。但就它提供的功能来说,`std::shared_ptr`的开销是非常合理的。在通常情况下,`std::shared_ptr`创建控制块会使用默认销毁器和默认分配器控制块只需三个word大小。它的分配基本上是无开销的。开销被并入了指向的对象的分配成本里。细节参见Item21。对`std::shared_ptr`解引用的开销不会比原始指针高。执行原子引用计数值修改操作需要承担一两个原子操作开销,但是这些操作通常都会一一映射到机器指令上,所以即使对比非原子指令来说原子指令开销较大,但是它们仍然只是单个指令。对于每个被`std::shared_ptr`指向的对象来说控制块中的虚函数机制开销通常只需要承受一次,即对象销毁的时候。
作为这些轻微开销的交换,你得到了动态分配资源自动管理生命周期的好处。大多数时候,比起手动管理,使用`std::shared_ptr`管理共享性资源都是非常合适的。如果你还在由于是否能承受`std::shared_ptr`带来的开销,那就再想想你是否需要共享资源。如果独占资源可行或者可能可行,用`std::unique_ptr`是一个更好的选择。它的性能profile更接近于原始指针并且从`std::unique_ptr`升级到`std::shared_ptr`也很容易,因为`std::shared_ptr`可以从`std::unique_ptr`上创建。
反之不行。当你的资源由`std::shared_ptr`管理,现在又想修改资源生命周期管理方式是没有办法的。即使引用计数为一,你也不能重新修改资源所有权,改用`std::unique_ptr`管理它。所有权和`std::shared_ptr`指向的资源之前签订的协议是“除非死亡否则永不分离”。不能离婚,不能废除,没有特许。
`std::shared_ptr`不能处理的另一个东西是数组。和`std::unique_ptr`不同的是,`std::shared_ptr`的API设计之初就是针对单个对象的没有办法`std::shared_ptr<T[]>`。一次又一次,“聪明”的程序眼踌躇于是否该使用`std::shared_ptr<T>`指向数组,然后传入自定义数组销毁器。(即`delete []`)。这可以通过变异,但是是一个糟糕的注意。一方面,`std::shared_ptr`没有提供`operator[]`重载,所以索引数组需要借助怪异的指针算术。另一方面,`std::shared_ptr`支持指向基类指针的转换,这对于单个对象来说有效,但是当用于数组类型时相当于在类型系统上开洞。(处于这个原因,`std::unique_ptr`禁止这种转换。。更重要的是C++11已经提供了很多内置数组的候选方案比如`std::array`,`std::vector`,`std::string`)。声明一个指向傻瓜数组的智能指针几乎总是标识着糟糕的设计。
记住:
+ `std::shared_ptr`为任意共享所有权的资源一种自动垃圾会搜的便捷方式
+ 较之于`std::unique_ptr``std::shared_ptr`对象通常大两倍,控制块会产生开销,需要原子引用计数修改操作
+ 默认资源销毁是通过**delete**,但是也支持自定义销毁器。销毁器的类型是什么对于`std::shared_ptr`的类型没有影响
+ 避免从原始指针变量上创建`std::shared_ptr`

View File

@ -34,7 +34,7 @@
11. [Item 17:理解特殊成员函数函数的生成](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item17.md)
4. 智能指针
1. [Item 18:对于独占资源使用std::unique_ptr](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md) __更新中__
2. [Item 19:对于共享资源使用std::shared_ptr](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item19.md) __更新中__
2. [Item 19:对于共享资源使用std::shared_ptr](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item19.md)
3. Item 20:对于类似于std::shared_ptr的指针使用std::weak_ptr可能造成悬置
4. Item 21:优先考虑使用std::make_unique和std::make_shared而非new
5. Item 22:当使用Pimpl惯用法请在实现文件中定义特殊成员函数