按理說reset的時候需要delete ptr就需要ptr的類型(錯了請指正),而shared_ptr的template type可以是incomplete type(錯誤請指正)。想請教一下shared_ptr是如何做到不需要析構函數還能編譯的,是動態獲得deleter的么?另外我看unique_ptr實現里static_assert了size(T) &> 0,請問是不是說incomplete type的大小是0呢?


在常見實現里,shared ptr把deleter(包括默認情況下的operator delete)放進control block,相當於做了一次type erasure,把deleter的類型從shared_ptr類型本身裡面擦下去。deleter的類型在control block的具體類型上,shared_ptr本身只持有一個control block基類的指針,通過虛函數來調用deleter。而因為shared_ptr構造的時候要求必須是complete type,control block已經知道怎麼析構了。世界和平,shared_ptr析構的時候就調用個虛函數,具體事情它不管的。

unique_ptr沒有control block,deleter type(包括默認的std::default_delete)直接做在unique_ptr一起了,這就導致unique_ptr的析構函數需要親手析構被管理的類型,因此析構函數必須看到complete type。然而反過來,因為構建的時候只需要保存下指針,所以unique_ptr構造的時候不需要看到complete type。這倆正好是反的。

C++標準並沒有規定這些實現細節,但是規定函數簽名和特性的時候,是考慮著比較合理的實現方式來寫標準的,到最後標準落下來之後也差不多只能這麼實現了。


詳細講一下std::unique_ptr的部分吧。

std::unique_ptr需要明確知道類型的析構函數

其實並不是如此。是默認的刪除器需要知道類型的析構函數。

unique_ptr類型是

template&< class T, class Deleter = std::default_delete&
&> class unique_ptr;

可以看到,deleter的類型是unique_ptr類型的一部分。在unique_ptr內部會保存類型為T*Deleter的成員 ,分別表示保存的裸指針和刪除器。假設內部是這麼實現的 (一般會運用空基類優化把Deleter的空間優化掉,libstdc++里把他們放進了一個tuple。這裡是被我簡化了):

private:
T* p;
Deleter del;

然後析構的時候就會這樣:

~unique_ptr()
{
del(p);
}

Deleter是默認的std::default_delete時,del(p)就會delete pdelete會調用析構函數。而delete一個不完整類型的指針是ub。在典型的實現中都會在delete前通過static_assert(sizeof(T) &> 0)做檢查。 sizeof 對 incomplete type求值會直接編譯出錯。

所以,當Deleter非默認時,就不一定需要知道類型的析構函數。比如

// A is incomplete type
class A;
auto Del = [] (A*) { };
std::unique_ptr& ptr;


而對於std::shared_ptr來說

按理說reset的時候需要delete ptr就需要ptr的類型(錯了請指正),而shared_ptr的template type可以是incomplete type(錯誤請指正)。

應該不太正確。。cppreference是這麼描述的:

std::shared_ptr may be used with an incomplete typeT. However, the constructor from a raw pointer (template& shared_ptr(Y*)) and the template&void reset(Y*) member function may only be called with a pointer to a complete type (note that std::unique_ptr may be constructed from a raw pointer to an incomplete type).

reset的時候需要類型完整。默認構造的時候允許是不完整類型。


這本質是個量變引起質變的過程。

對shared_ptr來說,除了封裝的raw_ptr外還要保存ref_cnt和weak_cnt,因此需要額外的存儲空間保存,gcc使用一個control block來存訪兩個cnt,shared_ptr中會保存control block的指針(因此shared ptr是兩個指針的大小)。如果需要用戶自定義的析構函數,可以在構造shared ptr時把析構函數的指針也放入control block,這樣在release的時候會自動調用這個函數,因此shared ptr的類型定義中可以不需要知道析構函數的類型。

而unique_ptr的最大不同在於它沒有control block,它只是簡單的封裝了下raw ptr,提供了控制構造函數而已,所以它的size就是一個指針。如果要像shared ptr那樣也保存析構函數的指針的話,只能引入control block,並增大object size,這會引起質變。shared ptr之所以沒這個問題是因為它從一開始就質變過了。


唉。。這事情看一看code不是很清楚么。。這麼久居然沒有人看過。。
  • unique_ptr和shared_ptr實現上就不一樣。
  • unique_ptr實現起來就是保存一個指針,和這個指針的析構方法(默認就是 delete ptr)。
    • 調用delete ptr,就需要ptr的完整類型。否則是undefined behaviour。不信的話,編譯一下下面這段code,正常的編譯器肯定有warning

#include &

struct Test;

int main(){
Test* a = nullptr;
delete a;
return 0;
}

    • unique_ptr的作者知道這麼寫是錯的,直接static_assert掉,讓編譯直接不過了。

  • shared_ptr保存的是一個控制塊的指針。控制塊包含的就是一個引用計數和一個原來對象的裸指針。
    • 兩個shared_ptr指向一個地址,其實就是兩個shared_ptr控制塊地址一樣,控制塊裡面的引用計數是2.
    • shared_ptr如果是默認初始化,或者初始化是nullptr的話。根本不會構造控制塊。也就是控制塊本身就是nullptr。
    • 於是根本就不會調用delete ptr。所以就沒事了。。

------------------------------------------

下面是我看code的記錄unique_ptr實現的時候
  • 記錄下來的是一個指針和Deletor的tuple https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/unique_ptr.h#L152 。
  • Deletor直接set成了default deleter https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/unique_ptr.h#L156
  • Deletor直接調用的就是`delete 類型`。不過是把undefined behavior給check了。

shared_ptr實現的時候
  • 直接把成員變數里的指針賦值成nullptr https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/tr1/shared_ptr.h#L545, ref_count設置成默認構造
  • ref_count這玩意就是shared_ptr的control block,包含一個引用計數,加一個真正的指針。*BUT*,這玩意默認的時候就是nullptr啊。。因為真正的指針也是nullptr啊。
  • 最後,析構的時候,因為control block就是空,根本不會調用到 delete 那句話上。


#include &

using namespace std;

struct C;

int main()
{
unique_ptr& *a;
}

之前腦殘了,應該是這個+_+bbb

unique_ptr一樣可以是incomplete type呃。

[劃掉]另外我不是特別確定這個事:C++里應該除了void以外沒有尺寸是0的類型...[/劃掉] imcomplete type size也是0

再另外(求你以後不要把問題寫成一坨),deleter和析構是兩碼事。而且shared_ptr是能指定deleter的。
推薦閱讀:
相关文章