通 常,這不成爲問題。Allocator擁有低層次的接口是因爲它們是低層次的概念:它們通常隱藏在容器類內部,而不屬於普通用戶的代碼。然而,有時你可能 不得不關心allocator:當你自己寫一個新的容器類的時候。Allocator比new/delete難用多了,因此,更容易發生錯誤。如果你寫的 代碼不得不使用allocator,如何確保代碼正確?運行期的測試不能證明不存在錯誤,只能證明錯誤的存在。儘管如此,測試還是很重要--你越快發現 bug,它就越早被修正。
所 有的標準容器類都接受一個allocator類作爲其模板參數;這個參數有一個默認值,比如std::vector<int>是 vector<int,std:: allocator<int> >的簡寫。如果我們寫了一個有額外正確性檢查的alocator類,那就可以用它代替std::allocator<int>來作爲 vector的第二模板參數。如果沒有bug的話,vector的行爲將會和以前一樣。(當然,除了額外的檢查將使它變慢。)
這篇文章將展示這樣一個debugging allocator類。它在其適用範圍內是有價值的,並且實現它也是使用allocator的一個有趣練習。
測試
我們想檢查什麼種類的錯誤?最重要的是:
l 傳給的deallocate()的內存確實是這個allocator分配的。
l 當deallocate()一塊內存時,歸還了與分配時相同的字節數。(和malloc與free不同,allocator不爲自己記錄這些信息。)
l 正deallocate()的內存,其類型與分配時相同。(雖然 allocator將內存分配與對象構造解耦了,但內存仍然是爲某個特定數據類型分配的。)
l 當在一塊內存中寫入數據時,我們不會越界。
l 從不試圖在同一位置構造兩個對象,並且也不會試圖銷燬同一對象兩次。
l 當deallocate()一塊內存時,確保先銷燬了其中的所有對象。
如我們將要看到的,我們的debugging allocator不會滿足所有要求。我們能檢查其中的一部分錯誤,而不是全部。
在debugging allocator背後的主意很簡單[注2]:每當allocate()一塊內存時,多分配一些,並在最初幾個字節中記錄一些額外信息。用戶看不到這塊 debug區域;我們傳給用戶的指針指向在此之後的內存。當傳回給我們一個指向我們分配的內存塊的指針時,我們可以減小其值以查看debug區域,並確認 內存塊是被正確使用的。
這 樣的設計有兩個隱藏問題。首先,不可能相當可移植地實現它。我們必須保持對齊:假設用戶數據的地址需要對齊,我們保留了一些字節而還要維持對齊。我們如何 知道該加多少?理論上,我們無法知道;語言沒有提供判斷對齊要求的機制。(也許這將會增加到未來的標準中)。實踐中,它不是一個嚴重的可移植性議題:在 double word上對齊所有東西,在目前絕大部分平臺上都足夠好了,並且在要求更嚴格的平臺上作相應修改也很容易。
第 二問題是,在這個設計裏,有一部分錯誤很容易檢查,而其它的就不行了。用戶通過allocate()獲得一塊內存,並把它傳給deallocate(), 於是,這個設計很容易檢查allocate()和deallocate()是被一致使用的。不幸的是,很難檢驗construct()和destory ()是被一致使用的。
問 題是通常傳給construct()和destory()的參數不是從allocate()返回的指針。如果寫下a.construct(p,x),那麼 p必須指向通過a分配出的內存塊內部--是指向內部,而不是指向(其開頭)。也許用戶通過p1 = a.allocate(1000)分配了足夠1000個元素的內存。這種情況下,我們不知道construc()的第一個參數是p1,p1 + 5,還是p1 + 178。我們無法找到需要的debugging信息,因爲沒法找到分配出的內存塊的開始地址。
這 個問題有兩個可能的解決方案,但都不是工作得很好。首先,明顯,我們可以放棄所有的debugging信息都一定要放在內存塊開始處的主意。但是,這不怎 麼能行,因爲我們將不得不將我們的信息與用戶數據混合存儲。這會破壞指針的運算法則:用戶無法在不知道我們的debugging信息的情況下從一個元素步 進到下一個元素。另外一個選擇是,我們可以另外維護一個額外的數據結構:比如,我們可以維護一個保存活動對象信息的std::set,用戶每用 construct()創建一個對象,就在set中增加一個地址,用戶每用destory()銷燬一個對象就移除這個地址。
這個技術簡單而優雅,而它不能工作的理由很微妙。問題是用戶不是必須使用相同的allocator來創建和銷燬對象:用戶只需要使用一個等價的allocator。思考一下下面的一系列操作:
l 用戶被給予了一個allocator, a1,然後作了它的一個拷貝,a2 。
l 用戶用a1.construct(p,x)創建了一個新對象。
l 用戶用a2.destroy()銷燬對象。
這個序列是合法的,但是維護活動對象列表的allocator會指出它是一個錯誤:我們將新的對象加入到a1的活動對象列表中,而當用a2銷燬對象時將找不到它。
可以通過在所有給定的allocator的拷貝間共享列表來繞過這個問題嗎?也許吧,但是我們將會陷入定義上的問題。如果:
my_allocator<int> a1;
my_allocator<double> a2(a1);
然後a1和a2應該共享相同的活動對象列表嗎?(答案看起來可能是“不”,那麼再my_allocator<int>(a2)時怎麼辦?) 我們也陷入實現上問題:在幕後被共享的對象需特別處理,尤其是在有併發問題時。
因爲這些問題,我已經簡單地放棄了在construct()和destory()上作實質性檢查的主意。這個版本的debugging allocator只在它們的參數上作最小程度的檢查,並不試圖確保destory()的參數被構造過,或這個對象只銷毀一次。
這 個debugging allocator的主要目的是確保allocate()和deallocate()被一致使用。當分配內存時,我們在每塊內存開始處保留了兩個word 的內存,並在其中記錄了內存塊中元素的數目,和一個起源於類型的hash碼(從類型的名字,特別是如typeid(T).name()所給出的)。然後我 們在內存塊結束的地方還保留了另外一個word,並存入了hash碼的另一個拷貝作爲哨兵。然後當deallocate()內存塊時,我們檢查已經儲存的 元素數目與傳入的參數相同,已及兩個hash碼都是正確的。我們調用assert()以便一個不一致的行爲將導致程序失敗。
這 沒有給予我們所期望的所有檢查,但它讓我們確保傳給deallocate()的內存是這個allocator分配的,並且是對相同的類型進行的分配和歸 還,數目也是一致的。它也對越界給予了有限的保護:在任一方向的越界一個的錯誤都會覆蓋哨兵,而這個錯誤將會在歸還時被檢測到。
一個Allocator Adaptor
到 現在爲止我都沒有展示任何代碼,因爲我還沒有描述debugging allocator的任何精確形式。一個簡單的選擇是基於malloc或std::allocator來實現debugging allocator。這樣的話,只需要一個模板參數:將要分配的對象的類型。這不如我們所期望得那麼通用:用戶無法使用一個自定義的allocator來 配合測試。爲了更通用,最好將debugging allocator寫成一個allocator adaptor。(這樣做的另外一個動機,我承認,是爲了教學目的:於是我們可以探索allocator adaptor的通用特徵。)
寫 一個allocator adaptor產生了兩個新的問題。第一是我們不能對正在適配的東西作任何假設。我們不能想當然地認爲Allocator::pointer的類型是T *,也不能認爲可以將東西放入未初始化的內存中(即使是內建數據類型的東西,如char和int)。我們必須虔誠地使用construct()和 destroy()。雖然討厭,但只要加些注意就行了。
第 二個問題是一個設計問題:我們的debugging allocator的模板參數看起來應該是什麼樣子?第一個想法可能是應該僅有一個模板參數:一個模板的模板參數。然而,這不足夠通用:模板的模板參數只 對特定數目的實參來說才比較好,而allocator沒有這樣的限定。模板的模板參數對默認allocator(std::allocator< T>)是夠了,但對有額外參數的用戶自定義allocator就不行了,如my_allocator<T,flags>。
那麼一個普通的模板參數怎麼樣?我們可能想寫:
template <class Allocator>
class debug_allocator;
快 了。用戶只需寫debug_allocator<std:: allocator<int> >。不幸的是,還有一個問題。allocator的value_type可能是void,某些情況下這很有用、很重要(我在以前的文章中描述過)。 如果用戶這麼寫了,會發生什麼?
typedef debug_allocator<std::allocator<int> > A;
typedef typename A::template rebind<void>::other A2;
問題是A2 的value_type是void,而allocator內部的一些東西對void是不成立的。例如,有一個reference的typedef,而 void &是沒有意義的;它會導致編譯錯誤。默認的allocator有一個特化,std::allocator<void>。沒有理由地, 我們也需要一個特化。
我 們沒法明確地表示出在Allocator的value_type是void時,我們需要debug_allocator< Allocator>的一個特化版本。但有一個次好的方法。我們可以給debug_allocator第二個模板參數,它默認是 Allocator::value_type,於是可以在第二模板參數是void時寫一個特化了。第二個模板參數完全是實現細節:用戶不需要顯式寫出來, 通過寫下(例如)debug_allocator<std:: allocator<int> >能推導出來。
當 我們有了這個方法後,將所有的東西集在一起就不難了:完整的 debug allocator見於List 1。 你可能會發現debug_allocator在需要檢查你的容器類對內存的使用時頗有用處,但更重要的是你能把它作爲原型。 debug_allocator上使用的實現技巧對你自己的allocator adaptor很有用處。
Listing 1: Complete implementation of the debugging allocator
template <class Allocator, class T = typename Allocator::value_type>
class debug_allocator {
public: // Typedefs from underlying allocator.
typedef typename Allocator::size_type size_type;
typedef typename Allocator::difference_type difference_type;
typedef typename Allocator::pointer pointer;
typedef typename Allocator::const_pointer const_pointer;
typedef typename Allocator::reference reference;
typedef typename Allocator::const_reference const_reference;
typedef typename Allocator::value_type value_type;
template <class U> struct rebind {
typedef typename Allocator::template rebind<U>::other A2;
typedef debug_allocator<A2, typename A2::value_type> other;
};
public: // Constructor, destructor, etc.
// Default constructor.
debug_allocator()
: alloc(), hash_code(0)
{ compute_hash(); }
// Constructor from an underlying allocator.
template <class Allocator2>
debug_allocator(const Allocator2& a)
: alloc(a), hash_code(0)
{ compute_hash(); }
// Copy constructor.
debug_allocator(const debug_allocator& a)
: alloc(a.alloc), hash_code(0)
{ compute_hash(); }
// Generalized copy constructor.
template <class A2, class T2>
debug_allocator(const debug_allocator<A2, T2>& a)
: alloc(a.alloc), hash_code(0)
{ compute_hash(); }
// Destructor.
~debug_allocator() {}
public: // Member functions.
// The only interesting ones
// are allocate and deallocate.
pointer allocate(size_type n, const void* = 0);
void deallocate(pointer p, size_type n);
pointer address(reference x) const { return a.address(x); }
const_pointer address(const_reference x) const {
return a.address(x);
}
void construct(pointer p, const value_type& x);
void destroy(pointer p);
size_type max_size() const { return a.max_size(); }
friend bool operator==(const debug_allocator& x,
const debug_allocator& y)
{ return x.alloc == y.alloc; }
friend bool operator!=(const debug_allocator& x,
const debug_allocator& y)
{ return x.alloc != y.alloc; }
private:
typedef typename Allocator::template rebind<char>::other
char_alloc;
typedef typename Allocator::template rebind<std::size_t>::other
size_alloc;
// Calculate the hash code, and store it in this->hash_code.
// Only used in the constructor.
void compute_hash();
const char* hash_code_as_bytes()
{ return reinterpret_cast<const char*>(&hash_code); }
// Number of bytes required to store n objects of type value_type.
// Does not include the overhead for debugging.
size_type data_size(size_type n)
{ return n * sizeof(value_type); }
// Number of bytes allocated in front of the user-visible memory
// block. Must be large enough to store two objects of type
// size_t, and to preserve alignment.
size_type padding_before(size_type)
{ return 2 * sizeof(std::size_t); }
// Number of bytes from the beginning of the block we allocate
// until the beginning of the sentinel.
size_type sentinel_offset(size_type n)
{ return data_size(n) + padding_before(n); }
// Number of bytes in the sentinel.
size_type sentinel_size()
{ return sizeof(std::size_t); }
// Size of the area we allocate to store n objects,
// including overhead.
size_type total_bytes(size_type n)
{ return data_size(n) + padding_before(n) + sentinel_size(); }
Allocator alloc;
std::size_t hash_code;
};
// Specialization when the value type is void. We provide typedefs
// (and not even all of those), and we save the underlying allocator
// so we can convert back to some other type.
template <class Allocator>
class debug_allocator<Allocator, void> {
public:
typedef typename Allocator::size_type size_type;
typedef typename Allocator::difference_type difference_type;
typedef typename Allocator::pointer pointer;
typedef typename Allocator::const_pointer const_pointer;
typedef typename Allocator::value_type value_type;
template <class U> struct rebind {
typedef typename Allocator::template rebind<U>::other A2;
typedef debug_allocator<A2, typename A2::value_type> other;
};
debug_allocator() : alloc() { }
template <class A2>
debug_allocator(const A2& a) : alloc(a) { }
debug_allocator(const debug_allocator& a) : alloc(a.alloc) { }
template <class A2, class T2>
debug_allocator(const debug_allocator<A2, T2>& a) :
alloc(a.alloc) { }
~debug_allocator() {}
private:
Allocator alloc;
};
// Noninline member functions for debug_allocator. They are not defined
// for the void specialization.
template <class Allocator, class T>
typename debug_allocator<Allocator, T>::pointer
debug_allocator<Allocator, T>::allocate
(size_type n, const void* = 0) {
assert(n != 0);
// Allocate enough space for n objects of type T, plus the debug
// info at the beginning, plus a one-byte sentinel at the end.
typedef typename char_alloc::pointer char_pointer;
typedef typename size_alloc::pointer size_pointer;
char_pointer result = char_alloc(alloc).allocate(total_bytes(n));
// Store the size.
size_pointer debug_area = size_pointer(result);
size_alloc(alloc).construct(debug_area + 0, n);
// Store a hash code based on the type name.
size_alloc(alloc).construct(debug_area + 1, hash_code);
// Store the sentinel, which is just the hash code again.
// For reasons of alignment, we have to copy it byte by byte.
typename char_alloc::pointer sentinel_area =
result + sentinel_offset(n);
const char* sentinel = hash_code_as_bytes();
{
char_alloc ca(alloc);
int i = 0;
try {
for ( ; i < sentinel_size(); ++i)
ca.construct(sentinel_area + i, sentinel[i]);
}
catch(...) {
for (int j = 0; j < i; ++j)
ca.destroy(&*(sentinel_area + j));
throw;
}
}
// Return a pointer to the user-visible portion of the memory.
pointer data_area = pointer(result + padding_before(n));
return data_area;
}
template <class Allocator, class T>
void debug_allocator<Allocator, T>::deallocate
(pointer p, size_type n) {
assert(n != 0);
// Get a pointer to the space where we put the debugging information.
typedef typename char_alloc::pointer char_pointer;
typedef typename size_alloc::pointer size_pointer;
char_pointer cp = char_pointer(p);
size_pointer debug_area = size_pointer(cp - padding_before(n));
// Get the size request and the hash code, and check for consistency.
size_t stored_n = debug_area[0];
size_t stored_hash = debug_area[1];
assert(n == stored_n);
assert(hash_code == stored_hash);
// Get the sentinel, and check for consistency.
char_pointer sentinel_area =
char_pointer(debug_area) + sentinel_offset(n);
const char* sentinel = hash_code_as_bytes();
assert(std::equal(sentinel, sentinel + sentinel_size(),
sentinel_area));
// Destroy our debugging information.
size_alloc(alloc).destroy(debug_area + 0);
size_alloc(alloc).destroy(debug_area + 1);
for (size_type i = 0; i < sentinel_size(); ++i)
char_alloc(alloc).destroy(sentinel_area + i);
// Release the storage.
char_alloc(alloc).deallocate(cp - padding_before(n), total_bytes(n));
}
template <class Allocator, class T>
void debug_allocator<Allocator, T>::construct(pointer p, const
value_type& x)
{
assert(p);
a.construct(p, x);
}
template <class Allocator, class T>
void debug_allocator<Allocator, T>::destroy(pointer p)
{
assert(p);
a.destroy(p);
}
template <class Allocator, class T>
void debug_allocator<Allocator, T>::compute_hash() {
const char* name = typeid(value_type).name();
hash_code = 0;
for ( ; *name != '/0'; ++name)
hash_code = hash_code * (size_t) 37 + (size_t) *name;
注:
[1] Matt Austern. "The Standard Librarian: What Are Allocators Good For?" C/C++ Users Journal C++ Experts Forum, December 2000, <www.cuj.com/experts/1812/austern.htm>.
[2] This debugging allocator is based on the one in the SGI library, <www.sgi.com/tech/stl>. The original version was written by Hans-J. Boehm.