allocator 的使用方法

C++標準庫中的Allocator有一個複雜而低層次的接口[注1]。和new與delete不同,它們將內存分配與對象構造解耦。和malloc與free不同,它們要求你明確正在分配的內存的數據類型和對象數目。

    通 常,這不成爲問題。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.

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章