Item 20:Use `std::weak_ptr` for `std::shared_ptr` like pointers that can dangle.
=========================
說起來有些矛盾,可以很方便的創建一個表現起來想`std::shared_ptr`的智能指針,但是它卻不會參于被指向資源的共享式管理。換句話說,一個類似于`std::shared_ptr`的指針不影響它所指對象的引用計數。這種類型的智能指針必須面臨一個`std::shared_ptr`未曾面對過的問題:它所指向的對象可能已經被析構。一個真正的智能指針通過持續跟蹤判斷它是否已經懸掛(dangle)來處理這種問題,懸掛意味著它指向的對象已經不復存在。這就是`std::weak_ptr`的功能所在
你可能懷疑`std::weak_ptr`怎么會有用,當你檢查了下`std::weak_ptr`的API之后,你會覺得更奇怪。它的API看起來一點都不智能。`std::weak_ptr`不能被解引用,也不能檢測判空。這是因為`std::weak_ptr`不能被單獨使用,它是`std::shared_ptr`作為參數的產物。
這種關系與生俱來,`std::weak_ptr`通常由一個`std::shared_ptr`來創建,它們指向相同的地方,`std::shared_ptr`來初始化它們,但是`std::weak_ptr`不會影響到它所指向對象的引用計數:
```cpp
auto spw = std::make_shared<Widget>();//spw 被構造之后
//被指向的Widget對象的引用計數為1
//(欲了解std::make_shared詳情,請看Item21)
...
std::weak_ptr<Widget> wpw(spw);//wpw和spw指向了同一個Widget,但是RC(這里指引用計數,下同)仍舊是1
...
spw = nullptr;//RC變成了0,Widget也被析構,wpw現在處于懸掛狀態
```
懸掛的std::weak_ptr可以稱作是過期了(expired),可以直接檢查是否過期:
```cpp
if(wpw.expired())... //如果wpw懸掛...
```
但是我們最經常的想法是:查看`std::weak_ptr`是否已經過期,如果沒有過期的話,訪問它所指向的對象。想的容易做起來難啊。因為`std::weak_ptr`缺少解引用操作,也就沒辦法寫完成這樣操作的代碼。即使又沒法做到,將檢查和解引用分開的寫法也會引入一個競態存在:在調用expired以及解引用操作之間,另外一個線程可能對被指向的對象重新賦值或者摧毀了最后一個指向對象的`std::shared_ptr`,這樣就導致了被指向的對象的析構。這種情況下,你的解引用操作會產生未定義行為。
我們需要的是將檢查`std::weak_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`會是null:
```cpp
std::shared_ptr<Widget> spw1 = wpw.lock();//如果wpw已經過期
//spw1的值是null
auto spw2 = wpw.lock();//結果同上,這里使用了auto
```
另外一種方式是以`std::weak_ptr`為參數,使用`std::shared_ptr`構造函數。這種情況下,如果`std::weak_ptr`過期的話,會有異常拋出:
```cpp
std::shared_ptr<Widget> spw3(wpw);//如果wpw過期的話
//拋出std::bad_weak_ptr異常
```
你可能會產生疑問,`std::weak_ptr`到底有啥用。下面我們舉個例子,假如說現在有一個工廠函數,根據一個唯一的ID,返回一個指向只讀對象的智能指針。根據Item 18關于工廠函數返回類型的建議,它應該返回一個`std::unique_ptr`:
```cpp
std::unique_ptr<const Widget> loadWidget(WidgetID id);
```
如果loadWidget調用的代價不菲(比如,它涉及到了文件或數據庫的I/O操作),而且ID的使用也比較頻繁,一個合理的優化就是再寫一個函數,不僅完成loadWidget所做的事情,而且要緩存loadWidget的返回結果。把每一個請求過的Widget對象都緩存起來肯定會導致緩存自身的性能出現問題,所以,一個合理的做法是當被緩存的Widget不再使用時將它銷毀。
對于這樣的一個帶有緩存的工廠函數,返回`std::unique_ptr`類型不是一個很好的選擇。可以確定的兩點是:調用者接收指向緩存對象的智能指針,調用者來決定這些緩存對象的生命周期;但是,緩存也需要一個指向所緩存對象的指針。因為當工廠函數的調用者使用完了一個工廠返回的對象,這個對象會被銷毀,對應的緩存項會懸掛,所以緩存的指針需要有檢測它現在是否處于懸掛狀態的能力。因此緩存使用的指針應該是std::weak_ptr類型,它有檢測懸掛的能力。這就意味著工廠函數的返回類型應該是`std::shared_ptr`,因為只有當一個對象的生命周期被`std::shared_ptr`所管理時,`std::weak_ptr`才能檢測它自身是否處于懸掛狀態。
下面是一個較快卻欠缺完美的緩存版本的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類型
//指向了被緩存的對象(如果對象不在緩存中則是null)
if(!objPtr){
objPtr = loadWidget(id);
cache[id] = objPtr;
}//如果不在緩存中,載入并且緩存它
return objPtr;
}
```
C++11利用了hash表容器(`std::unordered_map`),盡管它沒有提供所需的WidgetID哈希算法以及相等比較函數。
我為啥要說fastLoadWidget實現欠缺完美,因為它忽略了一個事實,緩存可能把一些已經過期的`std::weak_ptr`(對應的Widget不會被使用了,已經被銷毀了)。所以它的實現還可以再改善下,但是我們還是不要深究了,因為深究對我們繼續深入了解`std::weak_ptr`沒有用處。我們下面探究第二個使用`std::weak_ptr`的場景:在觀察者模式中,主要的組成部分是:狀態可能會發生變化的subjects,以及當狀態變化時需要得到通知的observers.在大多數實現中,每一個subject包含了指向它的observers的數據成員.這就使得subject很容易發送出狀態變化的通知。subject對于控制他們的observer的生命周期(observer何時被析構)毫無興趣.但是,它們必須知道,如果一個observer析構了,subject就不能嘗試去訪問它了。一個合理的設計是:每一個subject擁有一個`std::weak_ptr`,指向了它的observer,這樣在可以在訪問之間,先檢查一下指針是否處于懸掛狀態。
下面講到最后一個`std::weak_ptr`的例子,有這樣一個數據結構,包含A,B和C。A和C共享B的所有權,它們各自包含了一個`std::shared_ptr`指向B
![20-1.png]
如果現在有需要使B擁有反向指針指向A,那么指針應該是什么類型?
![20-2.png]
下面有三種選擇:
* __一個原生指針__。如果這么做,A如果被析構了,但是C會繼續指向B,B包含的指向A的指針現在處于懸掛狀態。而B對此毫不知情,所以B有可能不小心反引用了那個懸掛指針,這樣會產生未定義的行為。
* __一個`std::shared_ptr`__。在這種設計下,A和B包含了`std::shared_ptr`互相指向對方。結果就引發了一個`std::shared_ptr`的環(A指向B,B指向A),這個環會使得A和B都不能得到析構。即使程序其他的數據結構都不能訪問到A和B(例如,C如果不再指向B),A和B的引用計數仍然是1.如果這種情況發生了,A和B都會是內存泄露的情況,實際上,程序永遠無法再訪問到它們,它們也永遠無法得到回收。
* __一個`std::weak_ptr`__。這樣避免了以上所有的問題。如果A被回收,B指向它的指針將會懸掛,B也有能力檢測到這一狀態。此外,就算A和B互相指向對方,B的指針也不會影響到A的引用計數。當沒有`std::shared_ptr`指向A時,也不會阻止A的析構。
使用`std::weak_ptr`毫無疑問是最好的選擇。然而,值得注意的是,使用`std::weak_ptr`來破壞預期的`std::shared_ptr`形成的環不是那么普遍。在定義的比較嚴格的數據結構,比如說樹,子節點一般被父節點所擁有。當父節點被析構時,子節點也應該會被析構。從父節點指向子節點的鏈接因此最好使用std::unique_ptr.因為子節點不應該比父節點存在的時間過長,從子節點指向父節點的鏈接可以安全的使用原生指針來實現。因此也不會出現子節點解引用一個指向父節點的懸掛指針。
當然,并不是所有的以指針為基礎的數據結構都是嚴格的層級關系。如果不是的話,就像剛才所說的緩存以及觀察者列表的情形,使用`std::weak_ptr`是最棒的選擇了。
從效率的觀點來看,`std::weak_ptr`和`std::shared_ptr`的情況基本相同,。`std::weak_ptr`對象的大小和`std::shared_ptr`對象相同,它們都利用了同樣的控制塊(請看Item 19),并且諸如構造,析構以及賦值都涉及到引用計數的原子操作。這可能讓你吃了一驚,因為我在本章開始的時候說`std::weak_ptr`不參與引用計數的操作。可能沒有表達完整我的意思。我要寫的意思是`std::weak_ptr`不參與對象的共享所有權,因此不影響被指向對象的引用計數。但是,實際上在控制塊中存在第二個引用計數,`std::weak_ptr`來操作這個引用計數。欲知詳情,請看Item 21.
|要記住的東西|
|:--------- |
|`std::weak_ptr`用來模仿類似std::shared_ptr的可懸掛指針|
|潛在的使用`std::weak_ptr`的場景包括緩存,觀察者列表,以及阻止`std::shared_ptr`形成的環|
- 出版者的忠告
- 致謝
- 簡介
- 第一章 類型推導
- 條款1:理解模板類型推導
- 條款2:理解auto類型推導
- 條款3:理解decltype
- 條款4:知道如何查看類型推導
- 第二章 auto關鍵字
- 條款5:優先使用auto而非顯式類型聲明
- 條款6:當auto推導出非預期類型時應當使用顯式的類型初始化
- 第三章 使用現代C++
- 條款7:創建對象時區分()和{}
- 條款8:優先使用nullptr而不是0或者NULL
- 條款9:優先使用聲明別名而不是typedef
- 條款10:優先使用作用域限制的enmu而不是無作用域的enum
- 條款11:優先使用delete關鍵字刪除函數而不是private卻又不實現的函數
- 條款12:使用override關鍵字聲明覆蓋的函數
- 條款13:優先使用const_iterator而不是iterator
- 條款14:使用noexcept修飾不想拋出異常的函數
- 條款15:盡可能的使用constexpr
- 條款16:保證const成員函數線程安全
- 條款17:理解特殊成員函數的生成
- 第四章 智能指針
- 條款18:使用std::unique_ptr管理獨占資源
- 條款19:使用std::shared_ptr管理共享資源
- 條款20:在std::shared_ptr類似指針可以懸掛時使用std::weak_ptr
- 條款21:優先使用std::make_unique和std::make_shared而不是直接使用new
- 條款22:當使用Pimpl的時候在實現文件中定義特殊的成員函數
- 第五章 右值引用、移動語義和完美轉發
- 條款23:理解std::move和std::forward
- 條款24:區分通用引用和右值引用
- 條款25:在右值引用上使用std::move 在通用引用上使用std::forward
- 條款26:避免在通用引用上重定義函數
- 條款27:熟悉通用引用上重定義函數的其他選擇
- 條款28:理解引用折疊
- 條款29:假定移動操作不存在,不廉價,不使用
- 條款30:熟悉完美轉發和失敗的情況
- 第六章 Lambda表達式
- 條款31:避免默認的參數捕捉
- 條款32:使用init捕捉來移動對象到閉包
- 條款33:在auto&&參數上使用decltype當std::forward auto&&參數
- 條款34:優先使用lambda而不是std::bind
- 第七章 并發API
- 條款35:優先使用task-based而不是thread-based
- 條款36:當異步是必要的時聲明std::launch::async
- 條款37:使得std::thread在所有的路徑下無法join
- 條款38:注意線程句柄析構的行為
- 條款39:考慮在一次性事件通信上void的特性
- 條款40:在并發時使用std::atomic 在特殊內存上使用volatile
- 第八章 改進
- 條款41:考慮對拷貝參數按值傳遞移動廉價,那就盡量拷貝
- 條款42:考慮使用emplace代替insert