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/

相关文章
相关标签/搜索