本文翻譯自modern effective C++,由於水平有限,故無法保證翻譯完全正確,歡迎指出錯誤。謝謝!
博客已經遷移到這里啦
當你需要一個智能指針的時候,std::unique_ptr通常是最接近你需求的那一個。默認情況下,這么假設是很合理的:std::unique_ptr和原始指針的大小是一樣的,並且很多操作(包括解引用),它們執行的是完全相同的指令。這意味着你甚至能把它們用在對內存和時間都很緊的地方。如果一個原始指針對你來說足夠的小和快,那么一個std::unique_ptr也幾乎可以肯定是這樣的。
std::unique_ptr表現出獨占所有權的語義。一個非空的std::unique_ptr總是對它指向的資源擁有所有權。move一個std::unique_ptr將把所有權從源指針轉交給目標指針(源指針將被設置為null)。拷貝一個std::unique_ptr是不被允許的,因為如果你拷貝一個std::unique_ptr,你將得到兩個std::unique_ptr指向同樣的資源,然后這兩個指針都認為它們擁有資源(因此應該釋放資源)。因此std::unique_ptr是一個move-only(只能進行move操作的)類型。再看看資源的銷毀,一個非空的std::unique_ptr銷毀它的資源。默認情況下,通過在std::unique_ptr中delete一個原始指針的方法來進行資源的銷毀。
std::unique_ptr的常用方法是作為一個工廠函數的返回類型(指向類層次中的對象),假設我們有一個投資類型的類層次(比如,股票,債券,不動產等等),這個類層次的基類是Investment。
class Investment{ ... };
class Stock:
public Investment { ... };
class Bond:
public Investment { ... };
class RealRstate:
public Investmemt { ... };
對於這樣的類層次,一個工廠函數常常會在堆上分配一個對象,並且返回一個指向這個對象的指針,當這個對象不再需要被使用的時候,調用者有責任銷毀這個對象。這完全符合std::unique_ptr的概念,因為調用者要對工廠返回的資源負責(也就是,它獨占了所有權),然后當std::unique_ptr被銷毀的時候,std::unique_ptr會自動銷毀它指向的對象。對於Investment類層次,一個工廠函數能被聲明成這樣:
template<typename... Ts> //通過給定的參數,創建一個對象
std::unique_ptr<Investment> //然后,返回一個這個對象
makeInvestment(Ts&&... params); //的std::unique_ptr
調用者能在一個作用域中像下面這樣使用所返回的std::unique_ptr:
{
...
auto pInvestment = //pInvestment的類型是
makeInvestment( arguments ); //std::unique_ptr<Investment>
...
} //銷毀*pInvestment
但是他們也能把它用在“轉移所有權”的語義中,比如說當工廠返回的std::unique_ptr被move到容器中去了,容器中的元素接着被move到一個對象的成員變量中去了,然后這個對象之后會被銷毀。當這個對象被銷毀時,對象的std::unique_ptr成員變量也將被銷毀,然后它的銷毀會造成由工廠返回的資源被銷毀。如果由於一個異常或者其他的非正常的控制流(比如,在循環中return或break ),所有權鏈被打斷了,持有被管理資源的std::unique_ptr最終還是會調用它的析構函數,因此被管理的資源還是會被銷毀。
默認情況下,銷毀是通過delete進行的,但是,在銷毀的時候,std::unique_ptr對象能調用自定義的deleter(銷毀函數):當資源需要被銷毀的時候,任意的自定義函數(或仿函數,包括通過lambda表達式產生的仿函數)將被調用。如果由makeInvestment創造的對象不應該直接delete,而是需要先寫下日志記錄,makeInvestment能被實現成下面這樣(代碼后面跟着注釋,所以如果你看到一些不明確的代碼,不需要擔心)
//自定義deleter(一個lambda表達式)
auto delInvmt = [](Investment* pInvestment)
{
makeLogEntry(pInvestment);
delete pInvestment;
};
template<typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt)>
makeInvestment(Ts&&... params)
{
std::unique_ptr<investment, decltype(delInvmt)>
pInv(nullptr, delInvmt);
if( /* 一個股票對象需要被創建*/)
{
pInv.reset(new Stock(std::forward<Ts>(params)...));
}
else if( /* 一個債券對象需要被創建*/)
{
pInv.reset(new Bond(std::forward<Ts>(params)...));
}
else if( /* 一個不動產對象需要被創建*/)
{
pInv.reset(new RealEstate(std::forward<Ts>(params)...));
}
return pInv;
}
我馬上會解釋這是怎么工作的,但是現在,我們先考慮下如果你是一個調用者,你要做的事情看起來會怎么樣。假設把makeInvestment返回的結果存放在auto變量中,你是活在幸福中的,因為你不需要知道你使用的資源在銷毀時需要特殊對待。事實上,你真的是沐浴在幸福中,因為std::unique_ptr的使用意味着,當資源銷毀的時候你不需要關心它是怎么銷毀的,更不需要確保程序的每一條執行路徑中,資源都確實能進行銷毀。std::unique_ptr自動地把這些事情都做了。從一個客戶的角度來說,makeInvestment的接口是良好的。
一旦你理解了下面的東西,你會發現它的實現也是非常好的:
-
delInvmt是從makeInvestment返回的對象(std::unique_ptr對象)的自定義deleter,所有的自定義銷毀函數接受一個原始指針(這個指針指向需要被銷毀的資源),然后做一些在銷毀對象時必須做的事,我們的這種情況,函數的行為就是調用makeLogEntry並且調用delete。使用一個lambda表達式來創造delInvmt是很方便的,但是我們很快就能看到,比起一個傳統的函數來說,它更高效。
-
當一個自定義deleter被使用的時候,它的類型需要作為std::unique_ptr模板的第二個參數。我們的這種情況,就是delInvmt的類型,並且這也就是為什么makeInvestment的返回類型是std::unique_ptr<Investment, decltype(delInvmt)>。(關於decltype的信息,請看Item 3。)
-
makeInvestment最基本的策略是要創造一個null std::unique_ptr,然后讓它指向一個類型符合要求的對象,然后返回它。為了把自定義deleter delInvmt和pInv關聯起來,我們需要把它作為構造函數的第二個參數傳入。
-
嘗試把一個原始指針(比如,從new返回的)賦值給一個std::unique_ptr是無法通過編譯的,因為這將形成從原始指針到智能指針的隱式轉換,這樣的隱式轉換是有問題的,所以C++11的智能指針禁止這樣的轉換。這也就是為什么reset被用來:讓pInv獲得對象(通過new創建)的所有權。
-
對於每個new,我們使用std::forward來讓傳給makeInvestment的參數能完美轉發(看Item 25)。這使得當對象創建時,構造函數能獲得由調用者提供的所有信息。
-
自定義deleter需要一個
Investment*
類型的參數。不管makeInvestment中創造的對象的真正類型是什么(也就是,Stock,Bond或者RealEstate),它最終都能在lambda表達式中,作為一個Investment*
對象被delete掉。這意味着我們將通過一個基類指針delete一個派生類對象。為了讓這正常工作,基類(Investment)必須要有一個virutal析構函數:class Investment { public: ... virtual ~Investment(); ... };
在C++14中,由於函數返回值類型推導規則(看Item 3)的存在,意味着makeInvestment能被實現成更加簡潔以及更加封裝的方式:
/*
譯注:對於封裝來說,由於前面的形式必須要先知道delInvmt的實例才能
調用decltype(delInvmt)來確定它的類型,並且這個類型是只有編譯器知
道,我們是寫不出來的(看Item 5)。然后返回值的類型中又必須填寫
lambdas的類型,所以只能把lambda放在函數外面。
但是使用auto來進行推導就不需要這么做,即使把lambda表達式放里面,
也是可以由編譯器推導出來的。
*/
template<typename... Ts>
auto makeInvestment(Ts&&... params) //使用auto推導返回值類型
{
auto delInvmt = [](Investment* pInvestment)
{
makeLogEntry(pInvestment);
delete pInvestment;
};
//下面都和以前一樣
std::unique_ptr<investment, decltype(delInvmt)>
pInv(nullptr, delInvmt);
if( ... )
{
pInv.reset(new Stock(std::forward<Ts>(params)...));
}
else if( ... )
{
pInv.reset(new Bond(std::forward<Ts>(params)...));
}
else if( ... )
{
pInv.reset(new RealEstate(std::forward<Ts>(params)...));
}
return pInv;
}
我在之前就說過,當使用默認deleter(也就是,delete)時,你能合理地假設std::unique_ptr對象和原始指針的大小是一樣。當自定義deleter參合進來時,情況也許就不是這樣了。當deleter是函數指針的時候,通常會造成std::unique_ptr的大小從1個字節增加到2個字節(32位的情況下)。對於仿函數deleter,變化的大小依賴於仿函數中存儲的狀態有多少。沒有狀態的仿函數(比如,不捕獲變量的lambda表達式)遭受的大小的懲罰是0(不會改變大小),這意味着當自定義deleter能被實現為函數或lambda表達式時,lambda是更好的選擇:
auto delInvmt1 = [](Investment* pInvestment)
{
makeLogEntry(pInvestment);
delete pInvestment;
};
//這個函數返回的std::unique_ptr的大小和Investment*
//的大小一樣
template<typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt1)>
makeInvestment(Ts&&... args);
void delInvmt2(Investment* pInvestment)
{
makeLogEntry(pInvestment);
delete pInvestment;
};
//這個函數返回的std::unique_ptr的大小等於Investment*
//的大小加上一個函數指針的大小。
template<typename... Ts>
std::unique_ptr<Investment, void(*)(Investment*)>
makeInvestment(Ts&&... args);
帶大量狀態的仿函數deleter會產生大小很大的std::unique_ptr。如果你發現一個自定義deleter讓你的std::unique_ptr大到無法接受,你可能需要改變你的設計了。
工廠函數不是std::unique_ptr唯一的使用情況。它們在實現Pimpl機制的時候更加流行。這樣的代碼不是很復雜,但是也不是直截了當的,所以我會在Item 22中提及,那個Item是致力於這個話題的。
std::unique_ptr有兩種形式,一種是給單個對象(std::unique_ptr
operator*
和operator->)。
std::unique_ptr數組的存在應該只能作為你感興趣的技術,因為比起原始數組,std::array,std::vector以及std::string幾乎總是更好的數據結構的選擇。關於我能想象到的唯一的情景使得std::unique_ptr<T[]>是有意義的,那就只有當你使用類C的API時(並且它返回一個原始指針,指向堆上的數組,同時你擁有它的所有權)。
std::unique_ptr是在C++11中表達獨占所有權的方式,但是它最吸引人的特性是,它能簡單並高效地轉換到std::shared_ptr:
std::share_ptr<Investment> sp = //從std::unique_ptr轉換
makeInvestment( arguments ); //到std::shared_ptr
這就是為什么std::unique_ptr這么適合作為工廠函數的返回值類型的關鍵所在。工廠函數不知道調用者是否想要把對象用在獨占所有權的語義上還是共享所有權(也就是std::shared_ptr)的語義上。通過返回一個std::unique_ptr,工廠提供給調用者一個最高效的智能指針,但是他們不阻止調用者把它轉換成它更靈活的兄弟(std::shared_ptr)。(關於std::shared_ptr的信息,繼續看Item 19)
你要記住的事
- std::unique_ptr是一個小的,快的,mov-only的智能指針,它能用來管理資源,並且獨占資源的所有權。
- 默認情況下,資源的銷毀是用過delete進行的,但是自定義deleter能指定銷毀的行為。用帶狀態的deleter和函數指針作為deleter會增加std::unique_ptr對象的大小。
- 從std::unique_ptr轉換到std::shared_ptr很簡單。