#Item 19:使用std::shared_ptr來管理共享式的資源
使用垃圾回收機制的程序員指責并且嘲笑C++程序員阻止內存泄露的做法。“你們tmd是原始人!”他們嘲笑道。“你們有沒有看過1960年Lisp語言的備忘錄?應該用機器來管理資源的生命周期,而不是人類。”C++程序員開始翻白眼了:"你們懂個屁,如果備忘錄的內容意味著唯一的資源是內存而且回收資源的時機是不確定性的,那么我們寧可喜歡具有普適性和可預測性的析構函數."但是我們的回應部分是虛張聲勢。垃圾回收確實非常方便,手動來控制內存管理周期聽起來像是用原始工具來做一個記憶性的內存回路。為什么我們不兩者兼得呢?做出一個既可以想垃圾回收那樣自動,且可以運用到所有資源,具有可預測的回收時機(像析構函數那樣)的系統。
`std::shared_ptr`就是C++11為了達到上述目標推出的方式。一個通過`std::shared_ptr`訪問的對象被指向它的指針通過共享所有權(shared ownership)方式來管理.沒有一個特定的`std::shared_ptr`擁有這個對象。相反,這些指向同一個對象的`std::shared_ptr`相互協作來確保該對象在不需要的時候被析構。當最后一個`std::shared_ptr`不再指向該對象時(例如,因為`std::shared_ptr`被銷毀或者指向了其他對象),`std::shared_ptr`會在此之前摧毀這個對象。就像GC一樣,使用者不用擔心他們如何管理指向對象的生命周期,而且因為有了析構函數,對象析構的時機是可確定的。
一個`std::shared_ptr`可以通過查詢資源的引用計數(reference count)來確定它是不是最后一個指向該資源的指針,引用計數是一個伴隨在資源旁的一個值,它記錄著有多少個`std::shared_ptr`指向了該資源。`std::shared_ptr`的構造函數會自動遞增這個計數,析構函數會自動遞減這個計數,而拷貝構造函數可能兩者都做(比如,賦值操作`sp1=sp2`,sp1和sp2都是`std::shared_ptr`類型,它們指向了不同的對象,賦值操作使得sp1指向了原來sp2指向的對象。賦值帶來的連鎖效應使得原來sp1指向的對象的引用計數減1,原來sp2指向的對象的引用計數加1.)如果`std::shared_ptr`在執行減1操作后發現引用計數變成了0,這就說明了已經沒有其他的`std::shared_ptr`在指向這個資源了,所以`std::shared_ptr`直接析構了它指向的空間。
引用計數的存在對性能會產生部分影響
* `std::shared_ptrs`是原生指針的兩倍大小,因為它們內部除了包含了一個指向資源的原生指針之外,同時還包含了指向資源的引用計數
* 引用計數的內存必須被動態分配.概念上來說,引用計數會伴隨著被指向的對象,但是被指向的對象對此一無所知。因此,他們沒有為引用計數準備存儲空間。(一個好消息是任何對象,即使是內置類型,都可以被`std::shared_ptr`管理。)Item21解釋了用`std::make_shared`來創建`std::shared_ptr`的時候可以避免動態分配的開銷,但是有些情況下`std::make_shared`也是不能被使用的。不管如何,引用計數都是存儲為動態分配的數據
* 引用計數的遞增或者遞減必須是原子的,因為在多線程環境下,會同時存在多個寫者和讀者。例如,在一個線程中,一個`std::shared_ptr`指向的資源即將被析構(因此遞減它所指向資源的引用計數),同時,在另外一個線程中,一個`std::shared_ptr`指向了同一個對象,它此時正進行拷貝操作(因此要遞增同一個引用計數)。原子操作通常要比非原子操作執行的慢,所以盡管引用計數通常只有一個word大小,但是你可假設對它的讀寫相對來說比較耗時。
當我寫到:`std::shared_ptr`構造函數在構造時"通常"會增加它指向的對象的引用計數時,你是不是很好奇?創建一個新的指向某對象的`std::sharedptr`會使得指向該對象的`std::sharedptr`多出一個,為什么我們不說構造一個`std::sharedptr`總是會增加引用計數?
Move構造函數是我為什么那么說的原因。從另外一個`std::shared_ptr` move構造(Move-constructing)一個`std::shared_ptr`會使得源`std::shared_ptr`指向為null,這就意味著新的`std::shared_ptr`取代了老的`std::shared_ptr`來指向原來的資源,所以就不需要再修改引用計數了。Move構造`std::shared_ptr`要比拷貝構造`std::shared_ptr`快:copy需要修改引用計數,然而拷貝缺不需要。對于賦值構造也是一樣的。最后得出結論,move構造要比拷貝構造快,Move賦值要比copy賦值快。
像`std::unique_ptr`(Item 18)那樣,`std::shared_ptr`也把delete作為它默認的資源析構機制。但是它也支持自定義的deleter.然后,它支持這種機制的方式不同于`std::unique_ptr`.對于`std::unique_ptr`,自定義的deleter是智能指針類型的一部分,對于`std::shared_ptr`,情況可就不一樣了:
```cpp
auto loggingDel = [](widget *pw)
{
makeLogEntry(pw);
delete pw;
}//自定義的deleter(如Item 18所說)
std::unique_ptr<Widget, decltype(loggingDel)>upw(new Widget, loggingDel);//deleter類型是智能指針類型的一部分
std::shared_ptr<Widget> spw(new Widget, loggingDel);//deleter類型不是智能指針類型的一部分
```
std::shared_prt的設計更加的彈性一些,考慮到兩個std::shared_ptr<Widget>,每一個都支持不同類型的自定義deleter(例如,兩個不同的lambda表達式):
```cpp
auto customDeleter1 = [](Widget *pw) {...};
auto customDeleter2 = [](Widget *pw) {...};//自定義的deleter,屬于不同的類型
std::shared_prt<Widget> pw1(new Widget, customDeleter1);
std::shared_prt<Widget> pw2(new Widget, customDeleter2);
```
因為pw1和pw2屬于相同類型,所以它們可以放置到屬于同一個類型的容器中去:
```cpp
std::vector<std::shared_ptr<Widget>> vpw{ pw1, pw2 };
```
它們之間可以相互賦值,也都可以作為一個參數類型為`std::shared_ptr<Widget>`類型的函數的參數。所有的這些特性,具有不同類型的自定義deleter的`std::unique_ptr`全都辦不到,因為自定義的deleter類型會影響到`std::unique_ptr`的類型。
與`std::unique_ptr`不同的其他的一點是,為`std::shared_ptr`指定自定義的deleter不會改變`std::shared_ptr`的大小。不管deleter如何,一個`std::shared_ptr`始終是兩個pointer的大小。這可是個好消息,但是會讓我們一頭霧水。自定義的deleter可以是函數對象,函數對象可以包含任意數量的data.這就意味著它可以是任意大小。涉及到任意大小的自定義deleter的`std::shared_ptr`如何保證它不使用額外的內存呢?
它肯定是辦不到的,它必須使用額外的空間來完成上述目標。然而,這些額外的空間不屬于`std::shared_ptr`的一部分。額外的空間被分配在堆上,或者在`std::shared_ptr`的創建者使用了自定義的allocator之后,位于該allocator管理的內存中。我之前說過,一個`std::shared_ptr`對象包含了一個指針,指向了它所指對象的引用計數。此話不假,但是卻有一些誤導性,因為引用計數是一個叫做控制塊(control block)的很大的數據結構。每一個由`std::shared_ptr`管理的對象都對應了一個控制塊。改控制塊不僅包含了引用計數,還包含了一份自定義deleter的拷貝(在指定好的情況下).如果指定了一個自定義的allocator,也會被包含在其中。控制塊也可能包含其他的額外數據,比如Item 21條所說,一個次級(secondary)的被稱作是weak count的引用計數,在本Item中我們先略過它。我們可以想象出`std::shared_ptr<T>`的內存布局如下所示:
一個對象的控制塊被第一個創建指向它的`std::shared_ptr`的函數來設立.至少這也是理所當然的。一般情況下,函數在創建一個`std::shared_ptr`時,它不可能知道這時是否有其他的`std::shared_ptr`已經指向了這個對象,所以在創建控制塊時,它會遵循以下規則:
* `std::make_shared`(請看Item 21)總是會創建一個控制塊。它制造了一個新的可以指向的對象,所以可以確定這個新的對象在`std::make_shared`被調用時肯定沒有相關的控制塊。
* 當一個`std::shared_ptr`被一個獨占性的指針(例如,一個`std::unique_ptr`或者`std::auto_ptr`)構建時,控制塊被相應的被創建。獨占性的指針并不使用控制塊,所以被指向的對象此時還沒有控制塊相關聯。(構造的一個過程是,由`std::shared_ptr`來接管了被指向對象的所有權,所以原來的獨占性指針被設置為null).
* 當一個`std::shared_ptr`被一個原生指針構造時,它也會創建一個控制塊。如果你想要基于一個已經有控制塊的對象來創建一個`std::shared_ptr`,你可能傳遞了一個`std::shared_ptr`或者`std::weak_ptr`作為`std::shared_ptr`的構造參數,而不是傳遞了一個原生指針。`std::shared_ptr`構造函數接受`std::shared_ptr`或者`std::weak_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創建了第二個控制塊!
```
創建原生指針pw的行為確實不太好,這樣違背了我們一整章背后的建議(請看開章那幾段話來復習)。但是先不管這么多,創建pw的那行代碼確實不太建議,但是至少它沒有產生程序的未定義行為.
現在的情況是,因為spw1的構造函數的參數是一個原生指針,所以它為指向的對象(就是pw指向的對象:`*pw`)創造了一個控制塊(伴隨著一個引用計數)。到目前為止,代碼還沒有啥問題。但是隨后,spw2也被同一個原生指針作為參數構造,它也為`*pw`創造了一個控制塊(還有引用計數).`*pw`因此擁有了兩個引用計數。每一個最終都會變成0,最終會引起兩次對`*pw`的析構行為。第二次析構就要對未定義的行為負責了。
對于`std::shared_ptr`在這里總結兩點.首先,避免給std::shared_ptr構造函數傳遞原生指針。通常的取代做法是使用std::make_shared(請看Item 21).但是在上面的例子中,我們使用了自定義的deleter,這對于std::make_shared是不可能的。第二,如果你必須要給std::shared_ptr構造函數傳遞一個原生指針,那么請直接傳遞new語句,上面代碼的第一部分如果被寫成下面這樣:
```cpp
std::shared_ptr<Widget> spw1(new Widget,loggingDel);//direct use of new
```
這樣就不大可能從同一個原生指針來構造第二個`std::shared_ptr`了。而且,創建spw2的代碼作者會用spw1作為初始化(spw2)的參數(即,這樣會調用std::shared_ptr的拷貝構造函數)。這樣無論如何都不有問題:
```cpp
std::shared_ptr<Widget> spw2(spw1);//spw2 uses same control block as 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()
{
... //process the Widget
processedWidgets.emplace_back(this);//add it to list
//processed Widgets;
//this is wrong!
}
```
注釋里面說這樣做錯了,指的是傳遞this指針,并不是因為使用了`emplace_back`(如果你對`emplace_back`不熟悉,請看Item 42.)這樣的代碼會通過編譯,但是給一個`std::shared_ptr`傳遞this就相當于傳遞了一個原生指針。所以`std::shared_ptr`會給指向的Widget(*this)創建了一個新的控制塊。當你意識到成員函數之外也有`std::shared_ptr`早已指向了Widget,這就粗大事了,同樣的道理,會導致發生未定義的行為。
`std::shared_ptr`的API包含了修復這一問題的機制。這可能是C++標準庫里面最詭異的方法名字了:`std::enabled_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指針指向的對象,可以在其中使用`shared_from_this`。下面是Widget::process的一個安全實現:
```cpp
void Widget::process()
{
//as before, process the Widget
...
//add std::shared_ptr to current object to processedWidgets
processedWidgets.emplace_back(shared_from_this());
}
```
`shared_from_this`內部實現是,它首先尋找當前對象的控制塊,然后創建一個新的`std::shared_ptr`來引用那個控制塊。這樣的設計依賴一個前提,就是當前的對象必須有一個與之相關的控制塊。為了讓這種情況成真,事先必須有一個`std::shared_ptr`指向了當前的對象(比如說,在這個調用`shared_from_this`的成員函數的外面),如果這樣的`std::shared_ptr`不存在(即,當前的對象沒有相關的控制塊),雖然shared_from_this通常會拋出異常,產生的行為仍是未定義的。
為了阻止用戶在沒有一個`std::shared_ptr`指向該對象之前,使用一個里面調用`shared_from_this`的成員函數,繼承自`std::enable_shared_from_this`的子類通常會把它們的構造函數聲明為private,并且讓它們的使用者利用返回`std::shared_ptr`的工廠函數來創建對象。舉個栗子,對于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(); //as before
...
private:
... //構造函數
}
```
直到現在,你可能只能模糊的記得我們關于控制塊的討論源自于想要理解`std::shared_ptr`性能開銷的欲望。既然我們已經理解如何避免創造多余的控制塊,下面我們回歸正題吧。
一個控制塊可能只有幾個字節大小,盡管自定義的deleters和allocators可能會使得它更大。通常控制塊的實現會比你想象中的更復雜。它利用了繼承,甚至還用到虛函數(確保指向的對象能正確銷毀。)這就意味著使用`std::shared_ptr`會因為控制塊使用虛函數而導致一定的機器開銷。
當我們讀到了動態分配的控制塊,任意大小的deleters和allocators,虛函數機制,以及引用計數的原子操縱,你對`std::shared_ptr`的熱情可能被潑了一盆冷水,沒關系.它做不到對每一種資源管理的問題都是最好的方案。但是相對于它提供的功能,`std::shared_ptr`性能的耗費還是很合理。通常情況下,`std::shared_ptr`被`std::make_shared`所創建,使用默認的deleter和默認的allocator,控制塊也只有大概三個字節大小。它的分配基本上是不耗費空間的(它并入了所指向對象的內存分配,欲知詳情,請看Item 21.)解引用一個`std::shared_ptr`花費的代價不會比解引用一個原生指針更多。執行一個需要操縱引用計數的過程(例如拷貝構造和拷貝賦值,或者析構)需要一直兩個原子操作,但是這些操作通常只會映射到個別的機器指令,盡管相對于普通的非原子指令他們可能更耗時,但它們終究仍是單個的指令。控制塊中虛函數的機制在被`std::shared_ptr`管理的對象的生命周期中一般只會被調用一次:當該對象被銷毀時。
花費了相對很少的代價,你就獲得了對動態分配資源生命周期的自動管理。大多數時間,想要以共享式的方式來管理對象,使用`std::shared_ptr`是一個大多數情況下都比較好的選擇。如果你發現自己開始懷疑是否承受得起使用`std::shared_ptr`的代價時,首先請重新考慮是否真的需要使用共享式的管理方法。如果獨占式的管理方式可以或者可能實用,`std::unique_ptr`或者是更好的選擇。它的性能開銷于原生指針大致相同,并且從`std::unique_ptr`“升級”到s`td::shared_ptr`是很簡單的,因為`std::shared_ptr`可以從一個`std::unique_ptr`里創建。
反過來可就不一定好用了。如果你把一個資源的生命周期管理交給了`std::shared_ptr`,后面沒有辦法在變化了。即使引用計數的值是1,為了讓`std::unique_ptr`來管理它,你也不能重新聲明資源的所有權。資源和指向它的`std::shared_ptr`之間的契約至死方休。不許離婚,取消或者變卦。
還有一件事情`std::shared_ptr`不好用,那就是用在數組上面。可`std::unique_ptr`不同的一點就是,`std::shared_ptr`的API設計為指向單個的對象。沒有像`std::shared_ptr<T[]>`這樣的用法。經常有一些自作聰明的程序員使用`std::shared_ptr<T>`來指向一個數組,指定了一個自定義的deleter來做數組的刪除操作(即delete[]).這樣做可以通過編譯,但是卻是個壞主意,原因有二,首先,`std::shared_ptr`沒有重載操作符[],所以如果是通過數組訪問需要通過丑陋的基于指針的運算來進行,第二,`std::shared_ptr` supports derived-to-base pointer conversions that make sense for single objects, but that open holes in the type system when applied to arrays. (For this reason, the `std::unique_ptr<T[]>` API prohibits such conversions.)更重要的一點是,鑒于C++11標準給了比原生數組更好的選擇(例如,`std::array`,`std::vector`,`std::string`),給數組來聲明一個智能指針通常是不當設計的表現。
|要記住的東西|
|:--------- |
|`std::shared_ptr`為了管理任意資源的共享式內存管理提供了自動垃圾回收的便利|
|`std::shared_ptr`是`std::unique_ptr`的兩倍大,除了控制塊,還有需要原子引用計數操作引起的開銷|
|資源的默認析構一般通過delete來進行,但是自定義的deleter也是支持的。deleter的類型對于`std::shared_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