在第一部分中,我们介绍了new / delete的具体用法和背后的实现细节,这次我们将构建我们自己的小型工具集,可以使用我们自定义的allocator类来创建任意类型的实例(或者实例数组),我们需要做好准备,因为这里面涉及到了函数模板,type-based dispatching,模板黑魔法,以及一些巧妙的宏定义。

理想中,我们准备做的自定义内存系统需要创建实例的语法大概像下面这样:

假如我们定义了一个负责内存分配的类Arena

Arena arena; // one of many memory arenas

// ...
Test* test = new (arena, additionalInfo) Test(, , );
delete(test, arena); // no placement-syntax of delete // ...
Test* test = new (arena, additionalInfo) Test[];
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)(, , );
OM_DELETE(test, arena); // ...
Test* test = OM_NEW_ARRAY(Test[], arena);
OM_DELETE_ARRAY(test, arena);

接下来就让我们挨个实现这些宏,以及一些底层的函数,从最简单的开始吧。

OM_NEW

就像最普通的new operator一样,ME_NEW首先需要为给定的类型分配内存,然后在该内存上调用其构造函数。实现起来比较简单,就一行代码:

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

我们要做的就是在我们自定义的Arena.Allocate()函数返回的内存地址上使用placement new,同时也传递进去一些我们需要的信息,文件名,行号。另外需要特别注意的是我们最后的type,它的作用就是为了给构造函数提供构造所需的参数,可以在调用时,将参数附在宏的后面,如下所示:

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

// 宏展开后:

Test* test = new (om.Allocate(sizeof(Test), "test.cpp", )) Test(, , );

使用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 -: N
Bytes -: T[]
Bytes -: T[]
Bytes -: T[]

返回给用户的是指针式偏移了sizeof(size_t)个字节的地址。最终的使用方法如下:

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

这个还有个小问题,从上面的使用样例可以看出,因为类型T并没有出现在函数的参数列表中(只是用于函数的返回值类型),所以编译器无法帮助我们直接推导出类型,所以我们必须在每次使用时显式指定类型Test,但是如果我们用宏来包裹这个函数的话,在宏里我们并不知道实例的类型,同时在宏里我们也不知道实例的数量,先看下我们设想的宏的使用方式:

Test* test = OM_NEW_ARRAY(Test[], 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[-]; // call instances' destructor in reverse order
for (size_t i=N; i>; --i)
as_T[i-].~T(); arena.Free(as_size_t-);
}

根据注释大家基本可以理解原理了,宏的实现也比较简单:

#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/

C++ Memory System Part2: 自定义new和delete的更多相关文章

  1. 自定义new和delete

    #include "stdafx.h" #include <stdlib.h> #include <malloc.h> #include <iostr ...

  2. C++ Memory System Part3 : 优化

    前面的系列我们讲了自定义new和delete操作,其中针对deleteArray的问题还有需要优化的地方.我们这次就针对POD类型进行一次优化. 下面的代码是针对POD类型的模板函数实现,分别为New ...

  3. gem5: 使用ruby memory system中的mesh结构 出现AssertionError错误

    问题:在使用ruby memory system中的mesh结构測试时,出现例如以下错误: Traceback (most recent call last): File "<stri ...

  4. PatentTips - Mechanisms for strong atomicity in a transactional memory system

    BACKGROUND Advances in semi-conductor processing and logic design have permitted an increase in the ...

  5. Bit error testing and training in double data rate (ddr) memory system

    DDR PHY interface bit error testing and training is provided for Double Data Rate memory systems. An ...

  6. Power management in semiconductor memory system

    A method for operating a memory module device. The method can include transferring a chip select, co ...

  7. C++ Memory System Part1: new和delete

    在深入探索自定义内存系统之前,我们需要了解一些基础的背景知识,这些知识点是我们接下来自定义内存系统的基础.所以第一部分,让我们来一起深入了解一下C++的new和delete家族,这其中有很多令人吃惊的 ...

  8. armv8 memory system

    在armv8中,由于processor的预取,流水线, 以及多线程并行的执行方式,而且armv8-a中,使用的是一种weakly-ordered memory model, 不保证program or ...

  9. 自定义UITableViewCell 的delete按钮

    自定义UITableViewCell上的delete按钮 滑动列表行(UITableViewCell)出现删除按钮时,默认是英文“delete”,这份代码片段能够将“delete”变成中文”删除“,甚 ...

随机推荐

  1. window7 Oracle卸载步骤

    完全卸载oracle11g步骤:1. 开始->设置->控制面板->管理工具->服务(或 运行 services.msc) 停止所有Oracle服务.2. 开始->程序-& ...

  2. MVC Action控制方式

    1.Controller 的OnActionExecuting中控制 protected override void OnActionExecuting(ActionExecutingContext ...

  3. WPF中在摄像头视频上叠加控件的解决方案

    一.视频呈现 前段时间,在一个wpf的项目中需要实时显示ip摄像头,对此的解决方案想必大家都应该知道很多.在winform中,我们可以将一个控件(一般用panel或者pictruebox)的句柄丢给摄 ...

  4. C# 抽象(4)

    抽象类和普通类有区别嘛?在继承的时候,还是只能继承一次父类嘛? 答案: 抽象类除了在抽象方法和属性上面不能实现具体的代码之外和普通类没有区别.依旧符合类的基本特征.所以在继承的时候抽象类也是属于基类, ...

  5. Selenium API(二)

    1.定位一组元素 WebDriver提供了8种定位一组元素的方法. driver.find_elements_by_css_selector() driver.find_elements_by_tag ...

  6. c++实验2 顺序存储线性表

    线性表顺序存储 实现了动态数组的增删改查  前驱后继  A=AUB 动态数组右移 (1)顺序表存储结构的定义(类的声明): class SeqList { protected: DataType *l ...

  7. 利用keytool工具生成数字证书

    一.制作数字证书  因测试微信小程序, 腾讯要求使用 https协议,所以需要使用证书.使用jdk工具制作数字证书流程如下: 1.查看JDK是否安装,使用命令java -version 2.切换目录至 ...

  8. uoj#344. 【清华集训2017】我的生命已如风中残烛(计算几何)

    题面 传送门 题解 orzxyx 首先我们发现,一个点如果被到达大于一次,那么这个点肯定在一个环上.所以在不考虑环的情况下每个点只会被到达一次,那么我们就可以直接暴力了 简单来说,我们对每个点\(i\ ...

  9. Azure KUDU工具

    Azure网站提供了一个比较不错可以用来对我们的网站进行分析的工具------KUDU,下面我们就来看看这个工具主要能为我们做些啥,啥时候使用它. 如何打开KUDU KUDU所展现的强大功能 如何打开 ...

  10. R语言学习笔记(四)

    6. 数据转换 本章主要讲述apply系列函数:apply.lapply.sapply.tapply.mapply,以及姊妹函数by.split.适用于批量处理数据,而不许循环. 6.1 向量分组 用 ...