在第一部分中,我们介绍了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. java实现链式队列

    java实现链式队列...比较简单 package datastruct; public class QueueLink implements Queue { // 定义一个节点内部类 class N ...

  2. 用ActionBar的ActionProvider的时候报错:cannot be cast to android.view.ActionProvider

    在用ActionBar的自定义ActionProvider的时候有时候会遇到以下的报错:

  3. .NET和C#的版本历史

    维基百科页面:https://en.wikipedia.org/wiki/.NET_Framework_version_history Versionnumber CLRversion Release ...

  4. ST表略解

    题面 给定一个长度为\(N\)的数列,和\(M\)次询问,求出每一次询问的区间内数字的最大值. 对于30%的数据,满足: \(1≤N,M≤10\) 对于70%的数据,满足: \(1≤N,M≤10^5\ ...

  5. (转)使用vs调试的时候,如何知道程序阻塞在哪里?

    遇到一个问题,加了两个断点当运行到断点A后,我释放掉了,理想状态应该是在断点B停住,但并没有,程序感觉就像是阻塞了一样请问,这种状况如何知道程序当前是在哪里阻塞着? 回复: 可以让调试器停住,然后在调 ...

  6. MVC,MVP 和 MVVM 的区别之处

    其实我一直以来,虽然做的是前端的工作,但是有一个疑问,就是什么是mvc模式,虽然大概知道,但是具体确实说不上来的的,今天,我就好好总结一下mvc ,mvp,mvvm模式的区别与相同. 1.MVC模式: ...

  7. HDU6298-2018ACM暑假多校联合训练1001-Maximum Multiple

    题意大致是给你一个整数n,让你确定是否有三个正整数x,y,z既能被n整除,又能x+y+z=n,并使xyz最大 从中根据规律可以看出,只有被3或被4整除的数才能满足题目要求 被3整除的最大值为n^3/3 ...

  8. 【离散数学】 SDUT OJ 集合的包含

    集合的包含 Time Limit: 1000 ms Memory Limit: 65536 KiB Submit Statistic Problem Description 已知含n个元素的集合的子集 ...

  9. 关于 SimpleMembership 中 CreateDate 的问题

    使用 WebMatrix.WebData.WebSecurity.CreateUserAndAccount(model.UserName, model.Password,                ...

  10. 使用带参数的SQL语句向数据库中插入空值

    private void button1_Click(object sender, EventArgs e) { string name = textBox1.Text; int age = Conv ...