計數本體技術
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
用於在建構失敗時執行任何清理。請注意,並非所有編譯器都支援此功能。
使用 countable
的 new
表達式的結果是在堆上分配的物件,該物件具有一個保存計數的標頭區塊,也就是說,我們透過在其前面加上字首來擴展了該物件。我們可以在實作檔案中的匿名命名空間(未顯示)中提供一些功能,以支援計數以及從原始指標存取計數:
struct count { size_t value; }; count *header(const void *ptr) { return const_cast<count *>(static_cast<const count *>(ptr) - 1); }
這裡需要注意的一個重要約束是 count
的對齊方式,應使其適合任何類型。對於所示的定義,這在幾乎所有平台上都成立。但是,您可能需要為那些不符合的平台添加填充成員,例如使用匿名的 union
來使 count
與對齊要求最高的類型對齊。不幸的是,沒有可移植的方法可以指定此對齊,以確保也觀察到最小對齊要求 - 這是指定不直接使用 new
或 malloc
結果的自訂分配器時常見的問題。
再次注意,計數不被視為物件邏輯狀態的一部分,因此可以從 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[]
與 delete
或 malloc
與 delete
混用時發生的情況。Countable 一致性的重點在於 Countable 物件與 countable_ptr
一起使用,這確保了正確的使用方式。
然而,意外總是會發生,您可能不可避免地會忘記使用 new(countable)
進行分配,而是使用 new
。在大多數情況下,可以通過擴展這裡顯示的程式碼以在 count
中添加檢查成員來檢測此錯誤和其他錯誤,並在每次存取時驗證檢查。確保標頭和實作原始碼檔案之間有明確分隔的好處是,您可以引入此分配器的檢查版本,而無需重新編譯您的程式碼。
結論
本文介紹了兩個關鍵概念
- 使用基於泛型需求的方法來簡化和調整 COUNTED BODY 模式的使用。
- 通過控制分配,使用 RUNTIME MIXIN 模式動態且非侵入式地為固定類型添加功能的能力。
將兩者結合應用會產生基本 COUNTED BODY 模式的一種新變體,UNINTRUSIVE COUNTED BODY。您甚至可以進一步擴展這個主題,為 C++ 設計一個簡單的垃圾回收系統。
countable_ptr
、countability
和 countable new
的完整程式碼也可用。