Boost C++ 函式庫

...是世界上最受推崇且設計最精良的 C++ 函式庫專案之一。 Herb SutterAndrei AlexandrescuC++ 編碼標準

計數本體技術

Kevlin Henney
(kevlin@curbralan.com)

參考計數技術?您可能會認為這沒什麼新鮮的。每本好的 C++ 中級或進階教科書都會介紹這個概念。過去對它的探索如此徹底,以至於您可能會認為所有能說的都已經說完了。好吧,讓我們從第一原則開始,看看我們是否能挖掘出一些新的東西....

然後就沒有了...

參考計數背後的原理是保持一個物件的持續使用計數,以便當它降為零時,我們知道該物件不再被使用。這通常用於簡化動態分配物件的記憶體管理:保持對該物件的引用計數,當計數為零時,刪除該物件。

如何追蹤物件的使用者數量?好吧,普通的指標相當笨拙,因此需要一個額外的間接層來管理計數。這本質上是設計模式 [Gamma, Helm, Johnson & Vlissides, Addison-Wesley, ISBN 0-201-63361-2] 中描述的 PROXY 模式。其意圖如下:

為另一個物件提供代理或佔位符,以控制對它的存取。

Coplien [Advanced C++ Programming Styles and Idioms, Addison-Wesley, ISBN 0-201-56365-7] 定義了一組與句柄和主體部分這種基本分離相關的習慣用法。《Taligent Guide to Designing Programs》[Addison-Wesley, ISBN 0-201-40888-0] 確定了代理(又名替身)的許多特定類別。廣義上講,它們分為兩大類:

  • 隱藏式:句柄是感興趣的物件,隱藏了主體本身。句柄的功能是透過委託給主體來獲得,句柄的使用者並不知道主體的存在。參考計數字串提供了一種透明的優化。主體在字串的副本之間共享,直到需要更改時才建立副本。這種 COPY ON WRITE 模式(LAZY EVALUATION 的一種特殊形式)需要使用隱藏的參考計數主體。
  • 顯式:這裡主體是感興趣的,而句柄僅提供對其存取和內務處理的智能。在 C++ 中,這通常實現為 SMART POINTER 慣用法。其中一種應用是參考計數智能指標,它們協同工作來保持物件的計數,當計數降為零時將其刪除。

附加式與分離式

對於參考計數智能指標,計數可以存在於兩個地方,從而產生兩種不同的模式,這兩種模式都在Software Patterns [Coplien, SIGS, ISBN 0-884842-50-X] 中概述:

  • 計數本體或附加計數句柄/本體將計數放置在被計數的物件內。好處是可計數性是被計數物件的一部分,並且參考計數不需要額外的物件。缺點很明顯,這是侵入式的,並且當物件不是基於堆時,參考計數的空間會被浪費。因此,參考計數將您綁定到特定的實現和使用風格。
  • 分離計數句柄/本體將計數放置在被計數物件之外,以便它們可以一起處理。這樣做的好處很明顯,這種技術是完全非侵入式的,所有的智慧和支援設備都在智能指標中,因此可以用於獨立於參考計數指標建立的類別。主要的缺點是,頻繁使用這種方法會導致堆上創建大量小物件,即計數器。

即使經過這種簡單的分析,似乎分離計數句柄/本體方法更勝一籌。事實上,隨著範本的廣泛使用,這通常是首選,並且是常見(但非標準)的 counted_ptr 背後的原則。[Boost 的名稱是 shared_ptr 而不是 counted_ptr。]

COUNTED BODY 的常見實現是在計數類型所繼承的基底類別中提供計數機制。要麼這樣,要麼為每個需要它的類別重新提供參考計數機制。這兩種方法都不能令人滿意,因為它們相當封閉,將類別耦合到特定的框架中。除此之外,計數在非計數物件中處於休眠狀態的不一致性,讓您感覺到,除了在諸如 COM 和 CORBA 之類的廣泛物件模型中使用之外,COUNTED BODY 方法可能僅在特殊情況下才有用。

基於需求的方法

正是開放性的問題說服我重新審視 COUNTED BODY 慣用法的問題。是的,使用這種慣用法時,預期會有一定程度的侵入性,但有沒有辦法將其最小化,並將計數機制的選擇與使用的智能指標類型分離?

近年來,用於建構開放式通用元件的最具啟發性的程式碼和規格是 Stepanov 和 Lee 的 STL(標準範本庫),現在是 C++ 標準函式庫的一部分。STL 方法廣泛使用基於類型良好定義的操作要求的編譯時間多型。例如,每個容器、包含和迭代器類型都由應該對該類型的物件執行的操作來定義,通常帶有描述其他約束的註解。編譯時間多型,顧名思義,會根據函式名稱和參數使用(即多載)在編譯時解析函式。與基於類型、名稱和函式簽章的執行時間多型相比,這種方式的侵入性較小,儘管如果出錯則更難以診斷。

這種基於需求的方法可以應用於參考計數。我們需要類型為可計數的操作大致如下:

  • acquire 操作,用於註冊對可計數物件的興趣。
  • release 操作,用於取消註冊對可計數物件的興趣。
  • acquired 查詢,用於返回可計數物件目前是否已取得。
  • dispose 操作,負責處置不再取得的物件。

請注意,計數是作為此類型抽象狀態的一部分推導出來的,並且沒有以任何其他方式提及或定義。這種方法的開放性部分源於全域函式的使用,這意味著不暗示任何特定的成員函式;這是封裝現有的計數本體類別而不修改類別本身的完美方法。開放性的另一個方面來自對操作的更精確規範。

對於類型是可計數的,它必須滿足以下要求,其中 ptr 是指向該類型單一物件(即,不是陣列)的非空指標,而 #function 表示對 function(ptr) 的呼叫次數:

表達式 返回類型 語意和注意事項
acquire(ptr) 無要求 acquired(ptr)
release(ptr) 無要求 acquired(ptr)
acquired(ptr) == #acquire - #release
acquired(ptr) 可轉換為 bool 返回#acquire > #release
dispose(ptr, ptr) 無要求 !acquired(ptr)
*ptr 不再可用

請注意,dispose 的兩個參數旨在支援選擇要呼叫的適當類型安全版本函式。在一般情況下,意圖是第一個參數確定要刪除的類型,並且通常會使用範本,而第二個參數選擇要使用的範本,例如透過符合特定的基底類別。

此外,還必須滿足以下要求,其中 null 是指向可計數類型的空指標:

表達式 返回類型 語意和注意事項
acquire(null) 無要求 動作:無
release(null) 無要求 動作:無
acquired(null) 可轉換為 bool 返回false
dispose(null, null) 無要求 動作:無

請注意,這些函式在拋出或不拋出例外方面沒有任何要求,除非在拋出例外的情況下,這些函式本身應該是例外安全的。

變得聰明

給定類型的可計數要求,可以定義一個通用智能指標類型,它使用這些要求進行參考計數:

template<typename countable_type>
class countable_ptr
{
public: // construction and destruction

    explicit countable_ptr(countable_type *);
    countable_ptr(const countable_ptr &);
    ~countable_ptr();

public: // access

    countable_type *operator->() const;
    countable_type &operator*() const;
    countable_type *get() const;

public: // modification

    countable_ptr &clear();
    countable_ptr &assign(countable_type *);
    countable_ptr &assign(const countable_ptr &);
    countable_ptr &operator=(const countable_ptr &);

private: // representation

    countable_type *body;

};

為了闡述,此類別的介面保持有意簡單,例如,省略了成員範本和 throw 規格。大多數函式在實現中都相當簡單,非常依賴 assign 成員作為主要函式:

template<typename countable_type>
countable_ptr<countable_type>::countable_ptr(countable_type *initial)
  : body(initial)
{
    acquire(body);
}

template<typename countable_type>
countable_ptr<countable_type>::countable_ptr(const countable_ptr &other)
  : body(other.body)
{
    acquire(body);
}

template<typename countable_type>
countable_ptr<countable_type>::~countable_ptr()
{
    clear();
}

template<typename countable_type>
countable_type *countable_ptr<countable_type>::operator->() const
{
    return body;
}

template<typename countable_type>
countable_type &countable_ptr<countable_type>::operator*() const
{
    return *body;
}

template<typename countable_type>
countable_type *countable_ptr<countable_type>::get() const
{
    return body;
}

template<typename countable_type>
countable_ptr<countable_type> &countable_ptr<countable_type>::clear()
{
    return assign(0);
}

template<typename countable_type>
countable_ptr<countable_type> &countable_ptr<countable_type>::assign(countable_type *rhs)
{
    // set to rhs (uses Copy Before Release idiom which is self assignment safe)
    acquire(rhs);
    countable_type *old_body = body;
    body = rhs;

    // tidy up
    release(old_body);
    if(!acquired(old_body))
    {
        dispose(old_body, old_body);
    }

    return *this;
}

template<typename countable_type>
countable_ptr<countable_type> &countable_ptr<countable_type>::assign(const countable_ptr &rhs)
{
    return assign(rhs.body);
}

template<typename countable_type>
countable_ptr<countable_type> &countable_ptr<countable_type>::operator=(const countable_ptr &rhs)
{
    return assign(rhs);
}

公開責任

符合要求意味著類型可以與 countable_ptr 一起使用。以下是一個實作混合類別 (mix-imp),它透過成員函式將可計數性授予其衍生類別。這個類別可以用作類別適配器:

class countability
{
public: // manipulation

    void acquire() const;
    void release() const;
    size_t acquired() const;

protected: // construction and destruction

    countability();
    ~countability();

private: // representation

    mutable size_t count;

private: // prevention

    countability(const countability &);
    countability &operator=(const countability &);

};

請注意,操作函式是 const,並且 count 成員本身是 mutable。這是因為可計數性不是物件抽象狀態的一部分:記憶體管理不依賴物件的 const 性或其他。我不會在這裡包含成員函式的定義,因為您可能可以猜到它們:對於操作函式,分別是遞增、遞減和返回目前計數。在多執行緒環境中,您應該確保此類讀取和寫入操作是原子性的。

那麼,我們如何使這個類別可計數?一組簡單的轉發函式即可完成此操作:

void acquire(const countability *ptr)
{
    if(ptr)
    {
        ptr->acquire();
    }
}

void release(const countability *ptr)
{
    if(ptr)
    {
        ptr->release();
    }
}

size_t acquired(const countability *ptr)
{
    return ptr ? ptr->acquired() : 0;
}

template<class countability_derived>
void dispose(const countability_derived *ptr, const countability *)
{
    delete ptr;
}

現在繼承自 countability 的任何類型都可以與 countable_ptr 一起使用:

class example : public countability
{
    ...
};

void simple()
{
    countable_ptr<example> ptr(new example);
    countable_ptr<example> qtr(ptr);
    ptr.clear(); // set ptr to point to null
}   // allocated object deleted when qtr destructs

執行時間混合

挑戰是以非侵入性的方式應用 COUNTED BODY,以便在未計數物件時不會產生任何開銷。我們希望做的是在每個物件(而不是每個類別)的基礎上授予此能力。實際上,我們正在尋找任何物件上的可計數性,也就是任何由 void * 指向的物件!不用說,void 可能是任何類型中承諾最少的。

至少可以說,解決這個問題的力量非常有趣。有趣,但並非無法克服。鑑於執行時間物件的類別不能以任何明確定義的方式動態變更,並且物件的佈局必須是固定的,因此我們必須找到一個新的位置和時間來新增計數狀態。必須僅在堆建立時新增這一事實表明了以下解決方案:

struct countable_new;
extern const countable_new countable;

void *operator new(size_t, const countable_new &);
void operator delete(void *, const countable_new &);

我們已使用虛擬參數多載了 operator new,以區別於常規全域 operator new。這與標準函式庫中 std::nothrow_t 類型和 std::nothrow 物件的使用相當。placement operator delete 用於在建構失敗時執行任何清理。請注意,並非所有編譯器都支援此功能。

使用 countablenew 表達式的結果是在堆上分配的物件,該物件具有一個保存計數的標頭區塊,也就是說,我們透過在其前面加上字首來擴展了該物件。我們可以在實作檔案中的匿名命名空間(未顯示)中提供一些功能,以支援計數以及從原始指標存取計數:

struct count
{
    size_t value;
};

count *header(const void *ptr)
{
    return const_cast<count *>(static_cast<const count *>(ptr) - 1);
}

這裡需要注意的一個重要約束是 count 的對齊方式,應使其適合任何類型。對於所示的定義,這在幾乎所有平台上都成立。但是,您可能需要為那些不符合的平台添加填充成員,例如使用匿名的 union 來使 count 與對齊要求最高的類型對齊。不幸的是,沒有可移植的方法可以指定此對齊,以確保也觀察到最小對齊要求 - 這是指定不直接使用 newmalloc 結果的自訂分配器時常見的問題。

再次注意,計數不被視為物件邏輯狀態的一部分,因此可以從 const 轉換為非 const - count 實際上是一個 mutable 類型。

分配器函式本身相當簡單。

void *operator new(size_t size, const countable_new &)
{
    count *allocated = static_cast<count *>(::operator new(sizeof(count) + size));
    *allocated = count(); // initialise the header
    return allocated + 1; // adjust result to point to the body
}

void operator delete(void *ptr, const countable_new &)
{
    ::operator delete(header(ptr));
}

給定一個正確分配的標頭,我們現在需要 Countable 函式在 const void * 上操作以完成整個流程。

void acquire(const void *ptr)
{
    if(ptr)
    {
        ++header(ptr)->value;
    }
}

void release(const void *ptr)
{
    if(ptr)
    {
        --header(ptr)->value;
    }
}

size_t acquired(const void *ptr)
{
    return ptr ? header(ptr)->value : 0;
}

template<typename countable_type>
void dispose(const countable_type *ptr, const void *)
{
    ptr->~countable_type();
    operator delete(const_cast<countable_type *>(ptr), countable);
}

其中最複雜的是 dispose 函式,它必須確保正確的類型被解構,並且從正確的偏移量收集記憶體。它使用第一個引數的值和類型來正確執行此操作,而第二個引數僅作為策略選擇器,也就是說,使用 const void * 將其與先前顯示的用於 const countability * 的 dispose 區分開來。

變得更聰明。

現在我們有了一種在建立時為任何類型的物件添加計數能力的方法,要使其與我們之前定義的 countable_ptr 一起使用還需要什麼?好消息:什麼都不需要!

class example
{
    ...
};

void simple()
{
    countable_ptr<example> ptr(new(countable) example);
    countable_ptr<example> qtr(ptr);
    ptr.clear(); // set ptr to point to null
}   // allocated object deleted when qtr destructs

new(countable) 表達式為分配和釋放定義了不同的策略,並且與其他分配器一樣,任何混合使用分配策略的嘗試,例如在用 new(countable) 分配的物件上呼叫 delete,都會導致未定義的行為。這類似於您將 new[]deletemallocdelete 混用時發生的情況。Countable 一致性的重點在於 Countable 物件與 countable_ptr 一起使用,這確保了正確的使用方式。

然而,意外總是會發生,您可能不可避免地會忘記使用 new(countable) 進行分配,而是使用 new。在大多數情況下,可以通過擴展這裡顯示的程式碼以在 count 中添加檢查成員來檢測此錯誤和其他錯誤,並在每次存取時驗證檢查。確保標頭和實作原始碼檔案之間有明確分隔的好處是,您可以引入此分配器的檢查版本,而無需重新編譯您的程式碼。

結論

本文介紹了兩個關鍵概念

  • 使用基於泛型需求的方法來簡化和調整 COUNTED BODY 模式的使用。
  • 通過控制分配,使用 RUNTIME MIXIN 模式動態且非侵入式地為固定類型添加功能的能力。

將兩者結合應用會產生基本 COUNTED BODY 模式的一種新變體,UNINTRUSIVE COUNTED BODY。您甚至可以進一步擴展這個主題,為 C++ 設計一個簡單的垃圾回收系統。

countable_ptrcountabilitycountable new 的完整程式碼也可用。

首次發表於 Overload 25, 1998 年 4 月,ISSN 1354-3172