Alloctor— C++標準庫函數

來源:互聯網
上載者:User
  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);
   }
   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.

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.