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本身,其它的則是有各種修飾字的指針和引用。
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 &()的另外一種寫法:
現在我們能開始真正的工 作:allocate()和deallocate()。它們很簡單,但并不十分象malloc()和free()。我們傳給allocate()兩個參 數:我們正在為其分派空間的對象的數目(max_size返回可能成功的最大請求值),以及可選的一個地址值(可以被用作一個位置提示)。象 malloc_allocator這樣的簡單的allocator沒有利用那個提示,但為高性能而設計的allocator可能會利用它。返回值是一個指 向內存塊的指針,它足以容納n個value_type類型的對象并有正確的對齊。
我 們也傳給deallocate()兩個參數:當然一個是指針,但同樣還有一個元素計數值。容器必須自己掌握大小信息;傳給allocate和 deallocate的大小參數必須匹配。同樣,這個額外的參數是為效率而存在的,而同樣,malloc_allocator不使用它。
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()調用析構函數。
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的需求沒有包括賦值。只是基于安全,為了確保沒人偶然使用了賦值運算,我們將禁止掉這個可能自動生成的函數。
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對象是等價的:
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不能被老的編譯器支持,或支持得很差的原因。