#Item18:
當你要使用一個智能指針時,首先要想到的應該是`std::unique_ptr`.下面是一個很合理的假設:默認情況下,`std::unique_ptr`和原生指針同等大小,對于大多數操作(包括反引用),它們執行的底層指令也一樣。這就意味著,盡管在內存回收直來直往的情況下,`std::unique_ptr`也足以勝任原生指針輕巧快速的使用要求。
`std::unique_ptr`具現了獨占(exclusive ownership)語義,一個非空的`std::unique_ptr`永遠擁有它指向的對象,move一個`std::unique_ptr`會將所有權從源指針轉向目的指針(源指針指向為null)。拷貝一個`std::unique_ptr`是不允許的,假如說真的可以允許拷貝`std::unique_ptr`,那么將會有兩個`std::unique_ptr`指向同一塊資源區域,每一個都認為它自己擁有且可以摧毀那塊資源。因此,`std::unique_ptr`是一個move-only類型。當它面臨析構時,一個非空的`std::unique_ptr`會摧毀它所擁有的資源。默認情況下,`std::unique_ptr`會使用delete來釋放它所包裹的原生指針指向的空間。
`std::unique_ptr`的一個常見用法是作為一個工廠函數返回一個繼承層級中的一個特定類型的對象。假設我們有一個投資類型的繼承鏈。
[18-1.png]
```cpp
class Investment { ... };
class Stock:public Investment { ... };
class Bond:public Investment { ... };
class RealEstate:public Investment { ... };
```
生產這種層級對象的工廠函數通常在堆上面分配一個對象并且返回一個指向它的指針。當不再需要使用時,調用者來決定是否刪除這個對象。這是一個絕佳的`std::unique_ptr`的使用場景。因為調用者獲得了由工廠函數分配的對象的所有權(并且是獨占性的),而且`std::unique_ptr`在自己即將被銷毀時,自動銷毀它所指向的空間。一個為Investment層級對象設計的工廠函數可以聲明如下:
```cpp
template<typename... Ts>
std::unique_ptr<Investment> makeInvestment(Ts&&... params);// return std::unique_ptr
// to an object created
// from the given args
```
調用者可以在一處代碼塊中使用返回的`std::unique_ptr`:
```cpp
{
...
auto pInvestment = makeInvestment( arguments );
//pInvestment is of type std::unique_ptr<Investment>
...
}//destroy *pInvestment
```
他們也可以使用在擁有權轉移的場景中,例如當工廠函數返回的`std::unique_ptr`可以移動到一個容器中,這個容器隨即被移動到一個對象的數據成員上,該對象隨后即被銷毀。當該對象被銷毀后,該對象的`std::unique_ptr`數據成員也隨即被銷毀,它的析構會引發工廠返回的資源被銷毀。如果擁有鏈因為異常或者其他的異常控制流(如,函數過早返回或者for循環中的break語句)中斷,最終擁有資源的`std::unique_ptr`仍會調用它的析構函數(注解:這條規則仍有例外:大多數源自于程序的非正常中斷。一個從一個線程主函數(如程序的初始線程的main函數)傳遞出來的異常,或者一個違背了noexpect規范(請看Item 14)的異常,本地對象不會得到析構,如果`std::abort`或者其他的exit函數(如`std::_Exit`, `std::exit`,或者`std::quick_exit`)被調用,那么它們肯定不會被析構),`std::unique_ptr`管理的資源也因此得到釋放。
默認情況下,析構函數會使用delete。但是,我們也可以在它的構造過程中指定特定的析構方法(custom deleters):當資源被回收時,傳入的特定的析構方法(函數對象,或者是特定的lambda表達式)會被調用。對于我們的例子來說,如果被makeInvestment創建的對象不應該直接被deleted,而是首先要有一條log記錄下來,我們就可以這樣實現makeInvestment(當你看到意圖不是很明顯的代碼時,請注意看注釋)
```cpp
auto delInvmt = [](Investment* pInvestment){
makeLogEntry(pInvestment);
delete pInvestment;
};//custom deleter(a lambda expression)
template<typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt)>//revised return type
makeInvestment(Ts&&... params)
{
std::unique_ptr<Investment, decltype(delInvmt)> pInv(nullptr, delInvmt);//ptr to be returned
if ( /* a Stock object should be created */ )
{
pInv.reset(new Stock(std::forward<Ts>(params)...));
}
else if ( /* a Bond object should be created */ )
{
pInv.reset(new Bond(std::forward<Ts>(params)...));
}
else if ( /* a RealEstate object should be created */ )
{
pInv.reset(new RealEstate(std::forward<Ts>(params)...));
}
return pInv;
}
```
我之前說過,當使用默認的析構方法時(即,delete),你可以假設`std::unique_ptr`對象的大小和原生指針一樣。當`std::unique_ptr`用到了自定義的deleter時,情況可就不一樣了。函數指針類型的deleter會使得`std::unique_ptr`的大小增長到一個字節到兩個字節。對于deleters是函數對象的`std::unique_ptr`,大小的改變依賴于函數對象內部要存儲多少狀態。無狀態的函數對象(如,沒有captures的lambda expressions) 不會導致額外的大小開銷。這就意味著當一個自定義的deleter既可以實現為一個函數對象或者一個無捕獲狀態的lambda表達式時,lambda是第一優先選擇:
```cpp
auto delInvmt1 = [](Investment* pInvestment)
{
makeLogEntry(pInvestment);
delete pInvestment;
}
//custom deleter as stateless lambda
template<typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt1)>
makeInvestment(Ts&&.. args);//return type has size of Investment*
void delInvmt2(Investment* pInvestment)
{
makeLogEntry(pInvestment);
delete pInvestment;
}
template<typename... Ts>
std::unique_ptr<Investment,(void *)(Investment*)>
makeInvestment(Ts&&... params);//return type has size of Investment* plus at least size of function pointer!
```
帶有過多狀態的函數對象的deleters是使得`std::unique_ptr`的大小得到顯著的增加。如果你發現一個自定義的deleter使得你的`std::unique_ptr`大到無法接受,請考慮重新改變你的設計。
`std::unique_ptr`會產生兩種格式,一種是獨立的對象(std::unique_ptr<T>),另外一種是數組(`std::unique_ptr<T[]>`).因此,std::unique_ptr指向的內容從來不會產生任何歧義性。它的API是專門為了你使用的格式來設計的.例如,單對象格式中沒有過索引操作符(操作符[]),數組格式則沒有解引用操作符(操作符*和操作符->)
`std::unique_ptr`的數組格式對你來說可能是華而不實的東東,因為和原生的array相比,`std::array`,`std::vector`以及`std::string`幾乎是更好的數據結構選擇。我所想到的唯一的std::unique_ptr<T[]>有意義的使用場景是,你使用了C-like API來返回一個指向堆內分配的數組的原生指針,而且你像對之接管擁有權。
C++11使用`std::unique_ptr`來表述獨占所有權。但是它的一項最引人注目的特性就是它可以輕易且有效的轉化為`std::shared_ptr`:
```cpp
std::shared_ptr<Investment> sp = makeInvestment(arguments);//converts std::unique_ptr to std::shared_ptr
```
這就是`std::unique_ptr`很適合作為工廠函數返回值類型的原因。工廠函數不知道調用者想使用獨占性的擁有語義還是共享式的擁有語義(即`std::share_ptr`).通過返回`std::unique_ptr`,工廠函數將選擇權移交給了調用者,調用者在需要的時候可以將`std::unique_ptr`轉化為它最富有靈活性的兄弟(如果想了解更多關于`std::shared_ptr`,請移步Item 19)
|要記住的東西|
|:--------- |
|`std::unique_ptr`是一個具有開銷小,速度快,`move-only`特定的智能指針,使用獨占擁有方式來管理資源。|
|默認情況下,釋放資源由delete來完成,也可以指定自定義的析構函數來替代。但是具有豐富狀態的deleters和以函數指針作為deleters增大了`std::unique_ptr`的存儲開銷|
|很容易將一個`std::unique_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