在第一部分中,咱們介紹了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)爲例來講明一下它究竟是如何工做的:
首先是預處理的工做:
接下來是編譯器的工做:
就這樣,咱們將類型和數量兩個值傳遞到了宏,從而避免再傳遞多餘的參數給宏。
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/