在第一部分中,我们介绍了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. C# Task的使用

    1.Task的使用 创建一个Task,有三种方式 //第一种 Task t1 = new Task(() => { Console.WriteLine(DateTime.Now.ToString ...

  2. The 'microsoft.jet.oledb.4.0' provider is not registered on the local machin

    1,2选取目标站点,然后3的高级设置,4启用32位的应用程序的属性变为true就可以了.当然,网络上还有其他的版本,你也可以尝试下. 原文地址:http://weblogs.asp.net/kenco ...

  3. Sample-Code:Bing Search API

    Demo link: http://code.msdn.microsoft.com/windowsazure/How-to-use-bing-search-API-4c8b287e Aspx Code ...

  4. Python3之requests模块

    Python标准库中提供了:urllib等模块以供Http请求,但是,它的 API 太渣了.它是为另一个时代.另一个互联网所创建的.它需要巨量的工作,甚至包括各种方法覆盖,来完成最简单的任务. 发送G ...

  5. 推荐分享一个牛X的自定义PHP加密解密类

    通俗点说,用它来进行加密,同一个字符串,每次进行加密,得出的结果都是不一样的,大大加强了数据安全性.同时还可设定加密后数据的有效期,简直牛掰了 #食用方法 将下面的第二份模块代码保存为 Mcrypt. ...

  6. if __name__ == '__main__'是什么意思?如何理解?看到一个很有用的解答

    小明.py 朋友眼中你是小明(__name__ == '小明'), 你自己眼中你是你自己(__name__ == '__main__'), 你编程很好, 朋友调你去帮他写程序(import 小明, 这 ...

  7. libcaffe.so.1.0.0: cannot open shared object file: No such file or directory 运行时报错

    caffe安装好后lib没有配置到/usr/lib或/usr/local/lib中,需手动配置: sudo vim ~/.bashrc export LD_LIBRARY_PATH=your_path ...

  8. JDK 5 ~ 10 新特性倾情整理!

    JDK 5 ~ 10 新特性倾情整理! 最近连 JDK11都在准备发布的路上了,大家都整明白了吗?也许现在大部分人还在用6-8,8的新特性都没用熟,9刚出不久,10-11就不用说了. 为了大家对JDK ...

  9. angularjs中向html页面添加内容节点元素代码段的两种方法

    第一种方式:原生JS向html页面添加内容节点元素代码段: <!DOCTYPE html> <html> <head> <meta charset=" ...

  10. Android 生成xml文件及xml的解析

    1.生成xml文件的两种方式 (1)采用拼接的方式生成xml(不推荐使用) (2)利用XmlSerializer类生成xml文件 package com.example.lucky.test52xml ...