[英]Design of (shared_ptr + weak_ptr) compatible with raw pointers
在C ++ 11中,有std::shared_ptr
+ std::weak_ptr
組合。 盡管它非常有用,但是它有一個令人討厭的問題:您不能輕易地從原始指針構造shared_ptr 。 由於此缺陷,此類智能指針通常變得“病毒式”:人們開始完全避免使用原始指針和引用,並在整個代碼中僅使用shared_ptr和weak_ptr智能指針。 因為無法將原始引用傳遞給需要智能指針的函數。
另一方面,存在boost::intrusive_ptr
。 它等效於std::shared_ptr
並且可以很容易地從原始指針構造,因為引用計數器包含在對象中。 不幸的是,它沒有弱伴侶,因此沒有辦法擁有非所有者引用,您可以檢查它們是否無效。 實際上,有些人認為intrusive_ptr的弱伴侶是不可能的 。
現在,有std::enable_shared_from_this
,它直接將weak_ptr嵌入到您的類中,以便您可以從指向對象的指針構造shared_ptr。 但是有一個小的限制(至少必須存在一個shared_ptr),並且它仍然不允許明顯的語法: std::shared_ptr(pObject)
。
另外,還有一個std::make_shared
,它在單個內存塊中分配引用計數器和用戶對象 。 這與intrusive_ptr的概念非常接近,但是可以獨立於引用計數塊來銷毀用戶的對象。 同樣,此概念有一個不可避免的缺點:只有在所有weak_ptr-s都消失了時,才會釋放整個內存塊(可能很大)。
主要問題是:如何創建一對shared_ptr / weak_ptr,這對std::shared_ptr
/ std::weak_ptr
和boost::intrusive_ptr
都有好處?
尤其是:
可以進行侵入,即要求用戶從給定的基類繼承一次。 也可以在對象已被銷毀時保留其內存。 擁有線程安全性非常好(除非效率太低),但是沒有它的解決方案也很有趣。 每個對象分配幾個內存塊是可以的,盡管每個對象最好有一個內存塊。
1-4和6點已經由shared_ptr / weak_ptr建模。
第5點毫無意義。 如果壽命是共享的,再有就是如果沒有有效的對象weak_ptr
存在,但一個shared_ptr
沒有。 任何原始指針將是無效的指針。 對象的生命周期已經結束。 該對象不再存在。
weak_ptr
不會使對象保持活動狀態,而是使控制塊保持活動狀態。 shared_ptr
使控制塊和受控對象都保持活動狀態。
如果您不想通過組合控制塊和受控對象來“浪費”內存,請不要調用make_shared
。
如果您不希望將shared_ptr<X>
通過病毒傳遞給函數,請不要傳遞它。 將引用或const引用傳遞給X
如果您打算在函數中管理生命周期,則只需在參數列表中提及shared_ptr
。 如果只想對shared_ptr指向的內容執行操作,則傳遞*p
或*p.get()
並接受[const]引用。
在對象上覆蓋new
以在對象實例之前分配一個控制塊。
這是偽入侵。 由於已知的偏移量,因此可以從原始指針轉換為原始指針。 可以毫無問題地銷毀該對象。
引用計數塊具有強計數和弱計數,以及用於銷毀該對象的功能對象。
缺點:它在多態方面不能很好地工作。
想象一下,我們有:
struct A {int x;};
struct B {int y;};
struct C:B,A {int z;};
然后我們以這種方式分配C
C* c = new C{};
並將其存儲在A*
:
A* a = c;
然后,我們將其傳遞給A的智能指針。 它期望控制塊緊接在a
指向的地址之前,但是由於B
在C
的繼承圖中存在於A
之前,因此那里有B
的實例。
這似乎不理想。
所以我們作弊。 我們再次替換new
。 但是它改為在某處通過注冊表注冊指針值和大小。 我們在那里存儲弱指針/強指針計數(等)。
我們依靠線性地址空間和類布局。 當我們有一個指針p
,我們只是尋找它在誰的地址范圍內。然后我們知道了強/弱計數。
通常,這具有可怕的性能,尤其是多線程,並且依賴於未定義的行為(對於未指向同一對象的指針進行指針比較,或者在這種情況下,指針指向的順序less
)。
從理論上講,可以實現shared_ptr
和weak_ptr
侵入版本,但是由於C ++語言的限制,這可能是不安全的。
兩個引用計數器(強和弱)存儲在托管對象的基類RefCounters
中。 任何智能指針(共享的或弱的)都包含指向托管對象的單個指針。 共享指針擁有對象本身,而共享+弱指針共同擁有對象的內存塊。 因此,當最后一個共享指針消失時,對象將被銷毀,但是只要有至少一個指向它的弱指針,它的內存塊就會保持活動狀態。 鑒於所有涉及的類型仍然從RefCounted
類繼承,因此轉換指針可以按預期工作。
不幸的是,在C ++中,通常禁止在對象被銷毀后與對象成員一起工作,盡管大多數實現應允許這樣做而不會出現問題。 有關此方法的易讀性的更多詳細信息,可以在此問題中找到。
這是智能指針工作所需的基類:
struct RefCounters {
size_t strong_cnt;
size_t weak_cnt;
};
struct RefCounted : public RefCounters {
virtual ~RefCounted() {}
};
這是共享指針定義的一部分(顯示如何銷毀對象以及如何釋放內存塊):
template<class T> class SharedPtr {
static_assert(std::is_base_of<RefCounted, T>::value);
T *ptr;
RefCounters *Counter() const {
RefCounters *base = ptr;
return base;
}
void DestroyObject() {
ptr->~T();
}
void DeallocateMemory() {
RefCounted *base = ptr;
operator delete(base);
}
public:
~SharedPtr() {
if (ptr) {
if (--Counter()->strong_cnt == 0) {
DestroyObject();
if (Counter()->weak_cnt == 0)
DeallocateMemory();
}
}
}
...
};
帶有示例的完整代碼在此處 。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.