標籤:
原文: http://cissco.iteye.com/blog/379093
協助我們理解allocator的原理
Allocator是C++語言標準庫中最神秘的部分之一。它們很少被顯式使用,標準也沒有明確出它們應該在什麼時候被使用。今天的allocator與最初的STL建議非常不同,在此過程中還存在著另外兩個設計--這兩個都依賴於語言的一些特性,而直到最近才在很少的幾個編譯器上可用。對allocator的功能,標準似乎在一些方面追加了承諾,而在另外一些方面撤銷了承諾。
這篇專欄文章將討論你能用allocator來做什麼以及如何定義一個自己的版本。我只會討論C++標準所定義的allocator:引入准標準時代的設計,或試圖繞過有缺陷的編譯器,只會增加混亂。
什麼時候不使用Allocator
C++標準中的Allocator分成兩塊:一個通用需求集(描述於§ 20.1.5(表 32)),和叫std::allocator的class(描述於§20.4.1)。如果一個class滿足表32的需求,我們就稱它為一個allocator。std::allocator類滿足那些需求,因此它是一個allocator。它是標準程式庫中的唯一一個預先定義allocator類。
每個 C++程式員都已經知道動態記憶體分配:寫下new X來分配記憶體和建立一個X類型的新對象,寫下delete p來銷毀p所指的對象並歸還其記憶體。你有理由認為allocator會使用new和delete--但它們沒有。(C++標準將::operator new()描述為“allocation function”,但很奇怪,allocator並不是這樣的。)
有關allocator的最重要的事實是它們只是為了一個目的:封裝STL容器在記憶體管理上的低層細節。你不應該在自己的代碼中直接調用allocator的成員函數,除非正在寫一個自己的STL容器。你不應該試圖使用allocator來實現operator new[];這不是allocator該做的。 如果你不確定是否需要使用allocator,那就不要用。
allocator是一個類,有著叫allocate()和deallocate()成員函數(相當於malloc和free)。它還有用於維護所分配的記憶體的輔助函數和指示如何使用這些記憶體的typedef(指標或參考型別的名字)。如果一個STL容器使用使用者提供的allocator來分配它所需的所有記憶體(預定義的STL容器全都能這麼做;他們都有一個模板參數,其預設值是std::allocator),你就能通過提供自己的allocator來控制它的記憶體管理。
這種柔性是有限制的:仍然由容器自己決定它將要申請多少記憶體以及如何使用它們。在容器申請更多的記憶體時,你能控制它調用那個低層函數,但是你不能通過使用allocator來讓一個vector行動起來像一個deque一樣。雖然如此,有時候,這個受限的柔性也很有用。比如,假設你有一個特殊的fast_allocator,能快速分配和釋放記憶體(也許通過放棄執行緒安全性,或使用一個小的局部堆),你能通過寫下std::list<T, fast_allocator<T> >而不是簡單的std::list<T>來讓標準的list使用它。
如果這看起來對你很陌生,那就對了。沒有理由在常規代碼中使用allocator的。
定義一個Allocator
關於allocator的這一點你已經看到了:它們是模板。Allocator,和容器一樣,有value_type,而且allocator的value_type必須要匹配於使用它的容器的value_type。這有時會比較醜陋:map的value_type相當複雜,所以顯式調用allocator的map看起來象這樣的,std::map<K,V, fast_allocator<std::pair<const K, V> > >。(像往常一樣,typedef會對此有協助。)
以一個簡單的例子開始。根據C++標準,std::allocator構建在::operator new()上。如果你正在使用一個自動化記憶體使用量跟蹤工具,讓std::allocator更簡單些會更方便。我們可以用malloc()代替::operator new(),而且我們也不考慮(在好的std::allocator實作中可以找到的)複雜的效能最佳化措施。我們將這個簡單的allocator叫作malloc_allocator 。
既然malloc_allocator的記憶體管理很簡單,我們就能將重點集中在所有STL的allocator所共有的樣板上。首先,一些類型:allocator是一個類模板,它的執行個體專為某個類型T分配記憶體。我們提供了一序列的typedef,以描述該如何使用此類型的對象:value_type指T本身,其它的則是有各種修飾字的指標和引用。
Cpp代碼
- template <class T> class malloc_allocator
-
- {
-
- public:
-
- typedef T value_type;
-
- typedef value_type* pointer;
-
- typedef const value_type* const_pointer;
-
- typedef value_type& reference;
-
- typedef const value_type& const_reference;
-
- typedef std::size_t size_type;
-
- typedef std::ptrdiff_t difference_type;
-
- ...
-
- };
這些類型與STL容器中的很相似,這不是巧合:容器類常常直接從它的allocator提取這些類型。
為什麼有這麼多的typedef?你可能認為pointer是多餘的:它就是value_type *。絕大部份時候這是對的,但你可能有時候想定義非傳統的allocator,它的pointer是一個pointer-like的class,或非標的廠商特定類型value_type __far *;allocator是為非標擴充提供的標準hook。不尋常的pointer類型也是存在address()成員函數的理由,它在malloc_allocator中只是operator &()的另外一種寫法:
Cpp代碼
- template <class T> class malloc_allocator
-
- {
-
- public:
-
- pointer address(reference x) const { return &x; }
-
- const_pointer address(const_reference x) const {
-
- return &x;
-
- }
-
- ...
-
- };
現在我們能開始真正的工作:allocate()和deallocate()。它們很簡單,但並不十分象malloc()和free()。我們傳給allocate()兩個參數:我們正在為其指派空間的對象的數目(max_size返回可能成功的最大請求值),以及可選的一個地址值(可以被用作一個位置提示)。象malloc_allocator這樣的簡單的allocator沒有利用那個提示,但為高效能而設計的allocator可能會利用它。傳回值是一個指向記憶體塊的指標,它足以容納n個value_type類型的對象並有正確的對齊。
我們也傳給deallocate()兩個參數:當然一個是指標,但同樣還有一個元素計數值。容器必須自己掌握大小資訊;傳給allocate和deallocate的大小參數必須匹配。同樣,這個額外的參數是為效率而存在的,而同樣,malloc_allocator不使用它。
Cpp代碼
- template <class T> class malloc_allocator
-
- {
-
- public:
-
- pointer allocate(size_type n, const_pointer = 0) {
-
- void* p = std::malloc(n * sizeof(T));
-
- if (!p)
-
- throw std::bad_alloc();
-
- return static_cast<pointer>(p);
-
- }
-
- void deallocate(pointer p, size_type) {
-
- std::free(p);
-
- }
-
- size_type max_size() const {
-
- return static_cast<size_type>(-1) / sizeof(value_type);
-
- }
-
- ...
-
- };
allocate()和deallocate()成員函數處理的是未初始化的記憶體,它們不構造和銷毀對象。語句a.allocate(1)更象malloc(sizeof(int))而不是new int。在使用從allocate()獲得的記憶體前,你必須在這塊記憶體上建立對象;在通過deallocate()歸還記憶體前,你需要銷毀那些對象。
C++語言提供一個機制以在特定的記憶體位置建立對象:placement new。如果你寫下new(p) T(a, b),那麼你正在調用T的建構函式產生一個新的對象,一如你寫的new T(a, b)或 T t(a, b)。區別是當你寫new(p) T(a, b),你指定了對象被建立的位置:p所指向的地址。(自然,p必須指向一塊足夠大的記憶體,而且必須是未被使用的記憶體;你不能在相同的地址構建兩個不同的對象。)。你也可以調用對象的解構函式,而不釋放記憶體,只要寫p->~T()。
這些特性很少被使用,因為通常記憶體的分配和初始化是一起進行的:使用未初始化的記憶體是不方便的和危險的。你需要如此低層的技巧的很少幾處之一就是你在寫一個容器類,於是allocator將記憶體的分配與初始化解耦。成員函數construct()調用placement new,而且成員函數destory()調用解構函式。
Cpp代碼
- template <class T> class malloc_allocator
-
- {
-
- public:
-
- void construct(pointer p, const value_type& x) {
-
- new(p) value_type(x);
-
- }
-
- void destroy(pointer p) { p->~value_type(); }
-
- ...
-
- };
(為什麼allocator有那些成員函數,什麼時候容器可以直接使用placement new?一個理由是要隱藏笨拙的文法,而另一個是如果寫一個更複雜的allocator時你可能想在構造和銷毀對象時construct()和destroy()還有其它一些副作用。比如,allocator可能維護一個所有當前使用中的物件的日誌。)
這些成員函數沒有一個是static的,因此,容器在使用allocator前做的第一件事就是建立一個allocator對象--也就是說我們應該定義一些建構函式。但是,我們不需要賦值運算:一旦容器建立了它的allocator,這個allocator就從沒想過會被改變。表32中的對allocator的需求沒有包括賦值。只是基於安全,為了確保沒人偶然使用了賦值運算,我們將禁止掉這個可能自動產生的函數。
Cpp代碼
- template <class T> class malloc_allocator
-
- {
-
- public:
-
- malloc_allocator() {}
-
- malloc_allocator(const malloc_allocator&) {}
-
- ~malloc_allocator() {}
-
- private:
-
- void operator=(const malloc_allocator&);
-
- ...
-
- };
這些建構函式實際上沒有做任何事,因為這個allocator不需要初始化任何成員變數。基於同樣的理由,任意兩個malloc_allocator都是可互換的;如果a1和a2的類型都是malloc_allocator<int>,我們可以自由地通過a1來allocate()記憶體然後通過a2來deallocate()它。我們因此定義一個比較操作以表明所有的malloc_allocator對象是等價的:
Cpp代碼
- template <class T>
-
- inline bool operator==(const malloc_allocator<T>&,
-
- const malloc_allocator<T>&) {
-
- return true;
-
- }
-
- template <class T>
-
- inline bool operator!=(const malloc_allocator<T>&,
-
- const malloc_allocator<T>&) {
-
- return false;
-
- }
你會期望一個allocator,它的不同對象是不可替換的嗎?當然--但很難提供一個簡單而有用的例子。一種明顯的可能性是記憶體池。它對大型的C程式很常見,從幾個不同的位置(“池”)分配記憶體,而不是什麼東西都直接使用malloc()。這樣做有幾個好處,其一是it only takes a single function call to reclaim all of the memory associated with a particular phase of the program。 使用記憶體池的程式可能定義諸如mempool_Alloc和mempool_Free這樣的工具函數,mempol_Alloc(n, p)從池p中分配n個位元組。很容易寫出一個mmepool_alocator以匹配這樣的架構:每個mempool_allocator對象有一個成員變數以指明它綁定在哪個池上,而mempool_allocator::allocate()將調用mempool_Alloc()從相應的池中擷取記憶體。[注1]
最後,我們到了allocator的定義體中一個微妙的部份:在不同的類型之間映射。問題是,一個allocator類,比如malloc_allocator<int>,全部是圍繞著單個value_type構建的:malloc_allocator<int>::pointer是int*,malloc_allocator<int>().allocate(1)返回足夠容納一個int對象的記憶體,等等。然而,通常,容器類使用malloc_allocator可能必須處理超過一個類型。比如,一個list類,不分配int對象;實際上,它分配list node對象。(我們將在下一段落研究細節。)於是,當你建立一個std::list<int, malloc_allocator<int> >時,list必須將malloc_allocator<int>轉變成為處理list_node類型的malloc_allocator。
這個機制稱為重綁定,它有二個部份。首先,對於給定的一個value_type是X1的allocator類型A1,你必須能夠寫出一個allocator類型A2,它與A1完全相同,除了value_type是X2。其次,對於給定的A1類型的對象a1,你必須能夠建立一個等價的A2類型對象a2。這兩部分都使用了成員模板,這也就是allocator不能被老的編譯器支援,或支援得很差的原因。
[轉載] 徹底學習STL中的Allocator