mirror of
https://github.com/CnTransGroup/EffectiveModernCppChinese.git
synced 2024-12-28 05:40:43 +08:00
item19:complete
This commit is contained in:
parent
5dd5a9b171
commit
654bea9d6e
@ -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`
|
@ -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惯用法,请在实现文件中定义特殊成员函数
|
||||
|
Loading…
Reference in New Issue
Block a user