c++ unique_ptr

  • unique_ptr是擁有獨立對象所有權語義的智能指針,換言之,一個 unique_ptr對象所擁有的指針只允許它自己佔有,不允許多個對象共享(這裏希望大家理解語義和語法規則的區別,從語義上來說unique_ptr的指針不允許共享,但c++的語法規則並不禁止這麼做,當然後果就是未定義的了。)
  • unique_ptr是一個模板類,其擁有兩個模板參數,第一個參數是該對象持有指針指向的類型,第二個參數是刪除器的類型。
  • unique_ptr有兩個版本,第一個版本是默認的管理單個對象的版本,第二個版本是通過偏特化實現的管理動態分配的數組的版本。在cppreference網站上這個模板類的聲明是這個樣子:

1.png

  • 在vs2017中它是這個樣子:

2.png

  • unique_ptr可以被移動構造和移動賦值,但不能被複制構造和複製賦值。(這就是擁有獨立對象所有權這一理念的實現形式之一)

3.png

  • 可以看到,在vs2017中unique_ptr模板類的複製構造函數和複製賦值函數已經被聲明爲delete。
  • unique_ptr對第二個模板參數,也就是刪除器類型具有如下要求:Deleter必須是函數對象(FunctionObject)或者函數對象的左值引用,或者是函數(function)的左值引用,其應該可以通過一個類型爲unique_ptr<T,Deleter>::pointer的參數被調用。這裏解釋一下什麼是FunctionObject,c++中的FunctionObject類型的對象可以被使用在call operator(就是()這個調用運算符)左邊。
  • 注意:只有非常量的unique_ptr能夠將其管理對象的所有權轉移到另外一個unique_ptr對象中。如果一個對象的生命週期由一個const std::unique_ptr所管理,它就會被限制到該指針創建的scope中。
  • 上面這段話說了什麼意思呢?我們看下面這樣的代碼:

4.png

  • 首先定義並初始化一個unique_ptr類型的對象,然後將其管理的指針轉移到另外一個對象new_ptr中,最後我們打印出new_ptr管理的int*對象指向的值。

5.png

  • 執行成功。那麼如果我們將ptr的類型加上常量屬性呢?

6.png

  • 會產生如下的一個編譯錯誤:

7.png

  • 這個錯誤是說,嘗試去調用unique_ptr的複製構造函數,而這個函數如上所說已經被我們刪除。在c++中,具有const屬性的rvalue expression並不能被右值引用所捕獲,其只能被常量左值引用所捕獲。在第二個版本中,std::move(ptr)返回的值是一個具有const屬性的xvalue的值,其只能被複制構造函數所捕獲。
  • unique_ptr可以被一個不完整類型T構造(其第一個模板參數T可以是不完整類型),如果使用默認的刪除器的話,在刪除器被調用的點處T類型必須要是完整的,這些點包括析構函數、移動賦值函數、reset成員函數(而對應的shared_ptr不能夠被一個不完整類型的指針構造,但是可以在T爲不完整類型處釋放)。
  • 如果T是某個基類B的派生類,那麼std::unique_ptr<T>將會被隱式的轉化爲std::unique_ptr<B>,而std::unique_ptr<B>的默認刪除器將會按照B類型來釋放指針,如果B的析構函數不是virtual的話將會導致未定義行爲。注意std::shared_ptr表現不同,即使基類的析構函數不是virtual的,其也可以調用正確的析構函數。
  • 該類具有的成員類型、函數和變量不在贅述,標準中寫的很清楚,見:https://en.cppreference.com/w...
  • 下面我們看一下vs2017中,unique_ptr的一些實現細節:

10.png

11.png

  • 兩個版本的unique_ptr都繼承自一個共同的基類_Unique_ptr_base,這也是一個模板類,第一個模板參數是持有指針對應的類型(這裏已經去除了單個對象和數組對象的區別,那麼如何在析構函數中調用正確的delete呢?往後看),第二個模板參數是刪除器的類型。

12.png

  • 標準中要求的pointer成員類型也是由該基類導出。

13.png

在聲明中可以看到,默認的刪除器類型是default_delete<_Ty>類型,其中default_delete也是一個模板類,該模板參數就是要刪除指針指向的類型。

14.png

  • _Unique_ptr_base模板類中,首先通過類型萃取得到去除引用的刪除器類型:_Dx_noref。可以看到,pointer類型由_Get_deleter_pointer_type這個模板類導出。

15.png

  • 這個類通過特化實現了這樣一個邏輯:如果_Dx_noref類型中有pointer的成員類型,則將其作爲type類型導出,否則將_Ty* 作爲type類型導出。導出的就是unique_ptr中的pointer類型了。也就是說,我們可以通過對刪除器中添加一個pointer的成員類型來定製化unique_ptr。
  • 對於pointer的類型,標準中有如下說明:

16.png

  • 如果std::remove_reference<Deleter>::type::pointer存在的話,就是這個類型,否則就是T*類型,這與源代碼是相一致的。但是還有一個限制:必須要符合NullablePointer。我們在標準中繼續查看NullablePointer的含義。NullablePointer類型是指 該類型的對象能夠與std::nullptr_t類型對象進行比較的類似於指針的對象。(建議看英文原文,翻譯過來很彆扭)
  • 好了我們接着看_Unique_ptr_base模板類,這個模板類中有這樣一個成員變量:

18.png

  • 這裏又引入一個新的模板類:_Compressed_pair。這個模板類其實就存儲了兩個對象,第一個是刪除器類型的對象,第二個是我們存儲的指針類型的對象。既然如此簡單我們爲什麼要專門再用一個模板類來做一層抽象呢?這裏其實對當刪除器類型是一個空類的情況做了一個優化。例如默認的刪除器,其實我們只需要調用它的某個成員函數即可,完全不必要存儲任何成員變量,但是c++中的空類(即沒有數據成員的類)會佔據一個內存字節(這是爲了讓對象的實例能相互識別,爲了讓每個實例在內存中都有獨一無二的地址),當這個空類作爲成員函數在另外一個類中存在時,由於內存對齊的原因會消耗更多的內存地址。看下面這個例子:

19.png

  • A是一個空類,其作爲B類中第一個數據成員,B中第二個數據成員爲int類型變量。由於int類型變量佔據4個字節,由於內存對齊的原因,a1和a2之間還有3個字節的空白。Sizeof(B)返回的結果是8個字節。
  • _Compressed_pair模板類就是針對這個現象做了一個優化,當刪除器類型是空類時,通過繼承的方式獲得其成員函數和成員類型等信息而不用額外的內存消耗。

20.png

  • _Compressed_pair模板類有三個模板參數,第一個是刪除器類型,第二個是存儲的指針對應的類型,第三個是爲了做特化的類型bool。如果刪除器類型是空類型並且是一個final類型(不能被繼承),就使用默認的版本,即私有繼承刪除器類型,僅僅有一個_Ty2類型的成員變量。當第三個模板參數求值爲false時,採用其特化版本:

21.png

  • 可以看到,當刪除器類型不是空類型時,則按照常規處理方法保持兩個成員變量即可。
  • 這裏花了一點篇幅主要介紹了unique_ptr內存優化的部分,接下來我們看下默認刪除器是如何進行刪除操作的。

22.png

  • default_delete同樣根據_Ty類型是單一類型還是數組類型做了特化處理。

23.png

24.png

  • default_delete有一個構造函數,該函數是一個模板函數,如果_Ty2 是_Ty1 的子類,則可以用_Ty2類型的刪除器刪除_Ty1類型的指針。
  • 對()運算符的重載則是刪除時真正調用的函數了,可以看到在單一類型的版本中調用了delete,而在數組類型的版本中調用了delete[]。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章