簡體   English   中英

與原始指針兼容的(shared_ptr + weak_ptr)設計

[英]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_ptrboost::intrusive_ptr都有好處?

尤其是:

  1. shared_ptr對對象的共享所有權進行建模,即,當指向該對象的最后一個shared_ptr被銷毀時,該對象將被銷毀。
  2. weak_ptr不會對對象的所有權建模,它可用於解決循環依賴問題。
  3. 可以檢查weak_ptr是否有效:當存在指向該對象的shared_ptr時有效。
  4. 可以從有效的weak_ptr構造shared_ptr。
  5. 可以從指向對象的有效原始指針構造weak_ptr。 如果存在至少一個弱指針仍指向該對象,則原始指針有效。 從無效的指針構造weak_ptr會導致未定義的行為。
  6. 像上面提到的現有系統一樣,整個智能指針系統應該是易於投射的。

可以進行侵入,即要求用戶從給定的基類繼承一次。 也可以在對象已被銷毀時保留其內存。 擁有線程安全性非常好(除非效率太低),但是沒有它的解決方案也很有趣。 每個對象分配幾個內存塊是可以的,盡管每個對象最好有一個內存塊。

  • 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指向的地址之前,但是由於BC的繼承圖中存在於A之前,因此那里有B的實例。

這似乎不理想。


所以我們作弊。 我們再次替換new 但是它改為在某處通過注冊表注冊指針值和大小。 我們在那里存儲弱指針/強指針計數(等)。

我們依靠線性地址空間和類布局。 當我們有一個指針p ,我們只是尋找它在誰的地址范圍內。然后我們知道了強/弱計數。

通常,這具有可怕的性能,尤其是多線程,並且依賴於未定義的行為(對於未指向同一對象的指針進行指針比較,或者在這種情況下,指針指向的順序less )。

從理論上講,可以實現shared_ptrweak_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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM