C++ Memory System Part2: 自定義new和delete

在第一部分中,咱們介紹了new / delete的具體用法和背後的實現細節,此次咱們將構建咱們本身的小型工具集,可使用咱們自定義的allocator類來建立任意類型的實例(或者實例數組),咱們須要作好準備,由於這裏面涉及到了函數模板,type-based dispatching,模板黑魔法,以及一些巧妙的宏定義。html

 

理想中,咱們準備作的自定義內存系統須要建立實例的語法大概像下面這樣:數組

假如咱們定義了一個負責內存分配的類Arenawordpress

Arena arena; // one of many memory arenas

// ...
Test* test = new (arena, additionalInfo) Test(0, 1, 2);
delete(test, arena); // no placement-syntax of delete

// ...
Test* test = new (arena, additionalInfo) Test[10];
delete[] (test, arena); // no placement-syntax of delete[]

咱們可讓new operator像這樣去工做,只須要重載operator new,而後使用placement new syntax便可。可是delete卻不能如上同樣,由於delete operator沒有placement-syntax,也就是說它只能接收一個參數,若是直接調用operator delete,咱們就會遇到上節咱們提到的operator delete[]的析構問題,咱們沒法寫出編譯器無關的,跨平臺的析構調用方法。函數

 

另外,咱們想要在new的時候傳遞一些額外的信息,像文件名、行號、類名、內存標籤等等,同時還x想盡可能保留C++原始的new operator調用語法,因此,咱們使用宏的方式來定義new operator,最終但願達到像下面的代碼段這樣,來使用自定義的new operator工具

Test* test = OM_NEW(Test, arena)(0, 1, 2);
OM_DELETE(test, arena);

// ...
Test* test = OM_NEW_ARRAY(Test[3], arena);
OM_DELETE_ARRAY(test, arena);

接下來就讓咱們挨個實現這些宏,以及一些底層的函數,從最簡單的開始吧。佈局

 

OM_NEW優化

 

就像最普通的new operator同樣,ME_NEW首先須要爲給定的類型分配內存,而後在該內存上調用其構造函數。實現起來比較簡單,就一行代碼:ui

#define OM_NEW(type, arena) new (arena.Allocate(sizeof(type), __FILE__, __LINE__)) type

 

咱們要作的就是在咱們自定義的Arena.Allocate()函數返回的內存地址上使用placement new,同時也傳遞進去一些咱們須要的信息,文件名,行號。另外須要特別注意的是咱們最後的type,它的做用就是爲了給構造函數提供構造所需的參數,能夠在調用時,將參數附在宏的後面,以下所示:spa

// Test is a class taking 3 ints in the constructor
Test* test = OM_NEW(Test, om)(0, 1, 2);

// 宏展開後:指針

Test* test = new (om.Allocate(sizeof(Test), "test.cpp", 123)) Test(0, 1, 2);

使用OM_NEW,咱們可使用自定義的內存分配函數,同時傳遞額外的信息給它。同時也能夠保留了new operator原始的語法。

 

OM_DELETE

 

每一個使用OM_NEW建立的實例,都須要調用OM_DELETE來刪除。切記一點,沒有placement形式的delete operator,因此咱們要麼直接調用operator delete,要麼就使用徹底不一樣的方法。不管是哪一種方法,都要確保調用實例的析構函數。咱們能夠經過將刪除操做延遲給一個help函數去執行來實現:

#define ME_DELETE(object, arena)  Delete(object, arena)

 

help函數使用的是模板函數的方式:

template<typename T, class ARENA>
voidDelete(T* object, ARENA& arena)
{
    // call the destructor first...
    object->~T();

    // ...and free the associated memory
    arena.Free(object);

}

編譯器會幫咱們推導出全部的類型參數,不須要咱們顯式指定任何模板參數。

 

OM_NEW_ARRAY

 

到這裏事情就變得稍微複雜了一些。咱們首先須要一個能夠爲N個實例分配內存的函數,同時可以使用placement new正確地調用構造函數。由於它須要適用全部類型,因此咱們仍是用函數模板的方式來實現:

template<typename T, class ARENA>
T* OM_NewArray_Helper(ARENA& arena, size_t N, const char* file, int line)
{
  union
  {
    void* as_void;
    size_t* as_size_t;
    T* as_T;
  };

  as_void = arena.Allocate(sizeof(T)*N + sizeof(size_t), file, line);

  // store number of instances in first size_t bytes
  *as_size_t++ = N;

  // construct instances using placement new
  constT* const onePastLast = as_T + N;
  while(as_T < onePastLast)
    new(as_T++) T;

  // hand user the pointer to the first instance
  return(as_T - N);

}

 

上面的註釋基本說明了代碼的原理,我這裏就提一點,就是咱們在給N個實例分配內存的時候,額外分配了大小爲sizeof(size_t)的空間,它的目的就是爲了保存實例的數量。假如咱們的sizeof(T) == 4,sizeof(size_t) == 4,那麼咱們分配出來的內存的佈局以下:

Bytes 0-3: N
Bytes 4-7: T[0]
Bytes 8-11: T[1]
Bytes 12-15: T[2]

 

返回給用戶的是指針式偏移了sizeof(size_t)個字節的地址。最終的使用方法以下:

Test* t = OM_NewArray_Helper<Test>(arena, 3, __FILE__, __LINE__);

 

這個還有個小問題,從上面的使用樣例能夠看出,由於類型T並無出如今函數的參數列表中(只是用於函數的返回值類型),因此編譯器沒法幫助咱們直接推導出類型,因此咱們必須在每次使用時顯式指定類型Test,可是若是咱們用宏來包裹這個函數的話,在宏裏咱們並不知道實例的類型,同時在宏裏咱們也不知道實例的數量,先看下咱們設想的宏的使用方式:

Test* test = OM_NEW_ARRAY(Test[3], arena);

 

爲了使咱們的宏可以像這樣工做,該如何定義它呢?

#define ME_NEW_ARRAY(type, arena) OM_NewArray_Helper<?>(arena, ?, __FILE__, __LINE__)

宏裏的問號就是咱們如今還缺失的信息,那麼如何獲取到這部分信息呢,這時候就是模板黑魔法發揮做用的時候了:

template<class T>
structTypeAndCount
{
};

template<class T, size_t N>
structTypeAndCount<T[N]>
{
  typedefT Type;
  staticconstsize_tCount = N;
};

第一個基礎模板TypeAndCount只定義了一個模板參數,別的什麼都沒有作,可是它卻提供了部分偏特化的方式將type從T[N]中分離出來,這樣N也能夠在編譯期獲取到,最後宏的定義就成了:

#define OM_NEW_ARRAY(type, arena) NewArray<TypeAndCount<type>::Type>(arena, TypeAndCount<type>::Count, __FILE__, __LINE__)

可能不少人對這個黑魔法感受到有點懵逼,因此下面以OM_NEW_ARRAY(Test[3],arena)爲例來講明一下它究竟是如何工做的:

首先是預處理的工做:

  • 宏的TypeAndCount<type>::Type部分將會替換爲TypeAndCount<Test[3]>::Type.
  • 宏的TypeAndCount<type>::Count部分將會替換爲TypeAndCount<Test[3]>::Count.

 

接下來是編譯器的工做:

  • TypeAndCount<type>::Type的局部偏特化會產生Test
  • TypeAndCount<type>::Count的局部偏特化會產生3

 

就這樣,咱們將類型和數量兩個值傳遞到了宏,從而避免再傳遞多餘的參數給宏。

 

ME_DELETE_ARRAY

 

一樣的,咱們須要一個函數,幫咱們實現幾個功能:一是按照反序調用實例的析構函數,而後刪除相應的內存。廢話少說,直接看實現:

 

template <typename T, class ARENA>
void DeleteArray(T* ptr, ARENA& arena, NonPODType)
{
  union
  {
    size_t* as_size_t;
    T* as_T;
  };

  // user pointer points to first instance...
  as_T = ptr;

  // ...so go back size_t bytes and grab number of instances
  const size_t N = as_size_t[-1];

  // call instances' destructor in reverse order
  for (size_t i=N; i>0; --i)
    as_T[i-1].~T();

  arena.Free(as_size_t-1);
}

 

根據註釋你們基本能夠理解原理了,宏的實現也比較簡單:

#define OM_DELETE_ARRAY(object, arena) DeleteArray(object, arena)

 

到這裏,咱們基本已經完成了咱們的目標,實現了POD類型和NON-POD類型的自定義new / delete家族函數,可是這裏面其實還有須要優化的地方,好比若是是POD類型的實例,咱們不須要調用它的構造/析構函數,因此咱們的NewArray和DeleteArray函數模板均可以優化。這能夠經過類型派遣來實現(type-based dispatching),這裏暫時不展開討論了,留待下節詳細介紹。

 

 

 

 

 

參考link:

 

https://stoyannk.wordpress.com/2018/01/10/generic-memory-allocator-for-c-part-3/

 

https://bitsquid.blogspot.com/2010/09/custom-memory-allocation-in-c.html

 

https://blog.molecular-matters.com/

相關文章
相關標籤/搜索