finish item 20

This commit is contained in:
wendajiang 2020-11-01 17:28:51 +08:00
parent d0ef2dc159
commit d857f33111
4 changed files with 61 additions and 33 deletions

View File

@ -5,56 +5,84 @@
你可能想知道什么时候该用`std::weak_ptr`。你可能想知道关于`std::weak_ptr`API的更多。它什么都好除了不太智能。`std::weak_ptr`不能解引用,也不能测试是否为空值。因为`std::weak_ptr`不是一个独立的智能指针。它是`std::shared_ptr`的增强。
这种关系在它创之时就建立了。`std::weak_ptr`通常从`std::shared_ptr`上创建。当从`std::shared_ptr`上创建`std::weak_ptr`时两者指向相同的地方,但是`std::weak_ptr`不会影响所指对象的引用计数:
这种关系在它创之时就建立了。`std::weak_ptr`通常从`std::shared_ptr`上创建。当从`std::shared_ptr`上创建`std::weak_ptr`时两者指向相同的对象,但是`std::weak_ptr`不会影响所指对象的引用计数:
```cpp
auto spw = // spw构造后
std::make_shared<Widget>(); // 指向Widget
// 引用计数(RC)为1. (参见
// Item 21对于std::make_shared的描述)
auto spw = // after spw is constructed
std::make_shared<Widget>(); // the pointed-to Widget's
// ref count(RC) is 1
// See Item 21 for in on std::make_shared
std::weak_ptr<Widget> wpw(spw); // wpw指向相同的Widget
// RC保持为1
std::weak_ptr<Widget> wpw(spw); // wpw points to same Widget as spw. RC remains 1
spw = nullptr; // RC 变为0并且
// Widget销毁
// wpw不再dangle
spw = nullptr; // RC goes to 0, and the
// Widget is destroyed.
// wpw now dangles
```
`std::weak_ptr`用**expired**来表示对象已经dangle。你可以用它直接做测试
```CPP
if (wpw.expired()) … // 如果wpw不再指向对象
```
但是通常你期望的是检查`std::weak_ptr`缺少解引用操作,没有办法写这样的代码。即使有,将检查和解引用分开会引入竞态条件:在调用**expired**和解引用操作之间,另一个线程可能对是否过期,如果没有过期就再获取所指对象。听起来比做起来容易。因为`std::weak_ptr`缺少解引用操作,没有办法写这样的代码。即使有,将检查和解引用分开会引入竞态条件:在调用**expired**和解引用操作之间,另一个线程可能对指向的对象重新赋值或者析构,并由此造成对象已析构。这种情况下,你的解引用将会产生未定义行为。
`std::weak_ptr`用**expired**来表示已经dangle。你可以用它直接做测试
你需要的是一个原子操作实现检查是否过期,如果没有过期就访问所指对象。有两种方式可以从`std::weak_ptr`上创建`std::shared_ptr`完成,具体用哪种取决于`std::weak_ptr`过期时你希望`std::shared_ptr`表现出什么行为。一种形式是`std::weak_ptr::lock`,它返回一个`std::shared_ptr`,如果`std::weak_ptr`过期这个`std::shared_ptr`为空:
```cpp
std::shared_ptr<Widget> spw1 = wpw.lock(); // 如果wpw过期
// spw1为空
auto spw2 = wpw.lock(); // 和上面一样只是使用auto
```CPP
if (wpw.expired()) … // if wpw doesn't point to an object
```
另一种形式是以`std::weak_ptr`为实参构造`std::shared_ptr`。这种情况中,如果`std::weak_ptr`过期,一个异常会抛出:
但是通常你期望的是检查`std::weak_ptr`是否已经失效,如果没有失效则访问其指向的对象。这做起来比较容易。因为缺少解引用操作,没有办法写这样的代码。即使有,将检查和解引用分开会引入竞态条件:在调用**expired**和解引用操作之间,另一个线程可能对指向的对象重新赋值或者析构,并由此造成对象已析构。这种情况下,你的解引用将会产生未定义行为。
你需要的是一个原子操作实现检查是否过期,如果没有过期就访问所指对象。这可以通过从`std::weak_ptr`创建`std::shared_ptr`来实现,具体有两种形式可以从`std::weak_ptr`上创建`std::shared_ptr`,具体用哪种取决于`std::weak_ptr`过期时你希望`std::shared_ptr`表现出什么行为。一种形式是`std::weak_ptr::lock`,它返回一个`std::shared_ptr`,如果`std::weak_ptr`过期这个`std::shared_ptr`为空:
```cpp
std::shared_ptr<Widget> spw3(wpw); // 如果wpw过期抛出std::bad_weak_ptr异常
std::shared_ptr<Widget> spw1 = wpw.lock(); // if wpw's expired, spw1 is null
auto spw2 = wpw.lock(); // same as above, but uses auto
```
另一种形式是以`std::weak_ptr`为实参构造`std::shared_ptr`。这种情况中,如果`std::weak_ptr`过期,会抛出一个异常:
```cpp
std::shared_ptr<Widget> spw3(wpw); // if wpw's expired, throw std::bad_weak_ptr
```
但是你可能还想知道为什么`std::weak_ptr`就有用了。考虑一个工厂函数它基于一个UID从只读对象上产出智能指针。根据Item18的描述工厂函数会返回一个该对象类型的`std::unique_ptr`
```cpp
std::unique_ptr<const Widget> loadWidget(WidgetID id);
```
如果调用**loadWidget**是一个昂贵的操作比如它操作文件或者数据库I/O并且对于ID来重复使用很常见一个合理的优化是除了完成**loadWidget**做的事情之外再缓存它的结果。当请求获取一个Widget时阻塞在缓存操作上这本身也会导致性能问题所以另一个合理的优化可以是当Widget不再使用的时候销毁它的缓存。
如果调用`loadWidget`是一个昂贵的操作比如它操作文件或者数据库I/O并且对于ID来重复使用很常见一个合理的优化是再写一个函数除了完成`loadWidget`做的事情之外再缓存它的结果。当请求获取一个Widget时阻塞在缓存操作上这本身也会导致性能问题所以另一个合理的优化可以是当Widget不再使用的时候销毁它的缓存。
对于可缓存的工厂函数,返回`std::unique_ptr`不是好的选择。调用者应该明确接受缓存后的对象,调用者也应该确这些对象的生命周期,但是缓存本身也需要一个指针指向它所缓的对象。缓存的执着需要弹出它缓的对象是否已经没了,因为当客户段用完工厂返回的对象后,那个对象将会销毁。因此,缓存指针应该是`std::weak_ptr`——一个可以探测所缓对象是否没了的指针。这意味着工厂函数的返回类型应该是`std::shared_ptr`,因为仅当对象生命周期由`std::shared_ptr`管理时`std::weak_ptr`才能探测是否该对象是否为没了
对于可缓存的工厂函数,返回`std::unique_ptr`不是好的选择。调用者接受缓存后的对象的只能指针,调用者也应该确这些对象的生命周期,但是缓存本身也需要一个指针指向它所缓的对象。缓存对象的指针需要知道它是否已经dangle因为当工厂客户端使用完工厂产生的对象后对象将被销毁关联的缓存条目会dangle。所以缓存应该使用`std::weak_ptr`这可以知道是否已经dangle。这意味着工厂函数返回值类型应该是`std::shared_ptr`,因为`std::weak_ptr`依赖`std::shared_ptr`
下面是一个粗制滥造的缓存版本的**loadWidget**实现:
下面是一个粗制滥造的缓存版本的`loadWidget`实现:
```cpp
std::shared_ptr<const Widget> fastLoadWidget(WidgetID id)
{
static std::unordered_map<WidgetID,
std::weak_ptr<const Widget>> cache;
auto objPtr = cache[id].lock(); // objPtr是std::shared_ptr
// 指向被缓对象(为空表示被缓对象为空)
if (!objPtr) { // 如果没在缓存中
objPtr = loadWidget(id); // 加载
cache[id] = objPtr; // 缓它
std::weak_ptr<const Widget>> cache; // 译者注:这里是高亮
auto objPtr = cache[id].lock(); // objPtr is std::shared_ptr to cached object (or null if object's not in cache)
if (!objPtr) { // if not in cache
objPtr = loadWidget(id); // load it
cache[id] = objPtr; // cache it
}
return objPtr;
}
```
```
这个实现使用了C++11的hash表容器`std::unordered_map`,尽管没有显式表明需要`WidgetID`哈希和相等性比较的能力。
`fastLoadWidget`的实现忽略了以下事实cache可能会累积`expired`的与已经销毁的`Widget`相关联的`std::weak_ptr`。可以改进实现方式,但不要花时间在不会引起对`std :: weak_ptr`的深入了解的问题上让我们考虑第二个用例观察者设计模式。此模式的主要组件是subjects状态可能会更改的对象和observers状态发生更改时要通知的对象。在大多数实现中每个subject都包含一个数据成员该成员持有指向其observer的指针。这使subject很容易发布状态更改通知。subject对控制observers的生命周期例如当它们被销毁时没有兴趣但是subject对确保observers被销毁时不会访问它具有极大的兴趣 。一个合理的设计是每个subject持有其observers的`std::weak_ptr`因此可以在使用前检查是否已经dangle。
作为最后一个使用`std::weak_ptr`的例子考虑一个持有三个对象A,B,C的数据结构A和C共享B的所有权因此持有`std::shared_ptr`
![image-20201101170753295](media/item20/image-20201101170753295.png)
假定从B指向A的指针也很有用。应该使用哪种指针
![image-20201101170921305](media/item20/image-20201101170921305.png)
有三种选择:
- **原始指针**。使用这种方法如果A被销毁但是C继续指向BB就会有一个指向A的悬垂指针。而且B不知道指针已经悬垂所以B可能会继续访问就会导致未定义行为
- **`std::shared_ptr`**。这种设计A和B都互相持有对方的`std::shared_ptr`,导致`std::shared_ptr`在销毁时出现循环。即使A和B无法从其他数据结构被访问比如C不再指向B每个的引用计数都是1.如果发升了这种情况A和B都被泄露程序无法访问它们但是资源并没有被回收。
- **`std::weak_ptr`**。这避免了上述两个问题。如果A被销毁B还是有dangle指针但是B可以检查。尤其是尽管A和B互相指向B的指针不会影响A的引用计数因此不会导致无法销毁。
使用`std::weak_ptr`显然是这些选择中最好的。但是,需要注意使用`std::weak_ptr`打破`std::shared_ptr`循环并不常见。在严格分层的数据结构比如树,子节点只被父节点持有。当父节点被销毁时,子节点就被销毁。从父到子的链接关系可以使用`std::unique_ptr`很好的表征。从子到父的反向连接可以使用原始指针安全实现,因此子节点的生命周期肯定短于父节点。因此子节点解引用一个悬垂的父节点指针是没有问题的。
当然不是所有的使用指针的数据结构都是严格分层的所以当发生这种情况时比如上面所述cache和观察者情况知道`std::weak_ptr`随时待命也是不错的。
从效率角度来看,`std::weak_ptr`与`std::shared_ptr`基本相同。两者的大小是相同的使用相同的控制块参见Item 19构造、析构、赋值操作涉及引用计数的原子操作。这可能让你感到惊讶因为本Item开篇就提到`std::weak_ptr`不影响引用计数。我写的是`std::weak_ptr`不参与对象的*共享所有权*,因此不影响指向对象的引用计数。实际上在控制块中还是有第二个引用计数,`std::weak_ptr`操作的是第二个引用计数。想了解细节的话继续看Item 21吧。
### 记住
- 像`std::shared_ptr`使用`std::weak_ptr`可能会dangle
- `std::weak_ptr`的潜在使用场景包括caching、observer lists、打破`std::shared_ptr`指向循环

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -36,7 +36,7 @@
4. 智能指针
1. [Item 18:对于独占资源使用std::unique_ptr](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md) __@wendajiang更新完成__
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可能造成dangle](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item20.md) __更新__
3. [Item 20:像std::shared_ptr一样使用std::weak_ptr可能造成dangle](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item20.md) __更新完成__
4. [Item 21:优先考虑使用std::make_unique和std::make_shared而非new](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item21.md) __由 @pusidun贡献__
5. [Item 22:当使用Pimpl惯用法请在实现文件中定义特殊成员函数](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item22.md) __由 @BlurryLight贡献__
5. 右值引用,移动语意,完美转发