C++库应该如何允许自定义分配器

How should C++ libraries allow custom allocators?

本文关键字:何允许 自定义 分配器 C++      更新时间:2023-10-16

在C语言中,库很容易允许用户通过使用全局函数指针来自定义内存分配,该指针指向行为应与malloc()类似的函数和行为应与free()类似的函数。例如,SQLite就使用这种方法。

C++使事情有点复杂,因为分配和初始化通常是融合的。从本质上讲,我们希望获得仅为库重写operator newoperator delete的行为,但实际上没有办法做到这一点(我很确定,但不是100%)。

在C++中应该如何做到这一点?

这里首先尝试一下用函数Lib::make<T>复制new表达式的一些语义的东西。

我不知道这是否有用,但只是为了好玩,这里有一个更复杂的版本,它还试图复制new[]表达式的语义。

这是一个面向目标的问题,所以我不一定要查看代码。如果有更好的方法可以做到这一点,就这么说,忽略链接。

(我所说的"分配器"只是指分配内存的东西。我指的不是STL分配器的概念,甚至不是为容器分配内存。)


为什么这可能是可取的:

这是Mozilla开发人员的一篇博客文章,认为库应该这样做。他给出了一些C库的例子,这些库允许库用户自定义库的分配。我查看了其中一个示例SQLite的源代码,发现该功能也用于通过故障注入进行内部测试。我没有写任何需要像SQLite那样防弹的东西,但它似乎仍然是一个明智的想法。如果没有别的,它允许客户端代码计算出"哪个库占用了我的内存,什么时候占用?"。

简单的答案:不要使用C++。抱歉,开玩笑。

但是,如果你想在C++中,跨越库/模块的边界,以一种完全通用的方式,对内存管理进行这种绝对的控制,你可能会陷入一些可怕的悲伤。我建议大多数人寻找不做这件事的理由,而不是寻找做这件事情的方法

多年来(实际上是几十年),我经历了许多次相同基本想法的迭代,从试图在全局级别天真地重载运算符new/new[]/delete/delete[],到基于链接器的解决方案,再到特定于平台的解决方案。实际上,我正处于您现在想要的位置:我有一个系统,可以让我查看每个插件分配的内存量。但我并不是通过你(最初也是我)想要的那种广义的方式达到这一点的。

C++使事情有点复杂,因为分配和初始化通常是融合的。

我想稍微扭转一下这句话:C++使事情变得复杂,因为初始化和分配通常是融合的。我所做的只是交换这里的顺序,但最复杂的部分不是分配想要初始化,而是因为初始化通常想要分配。

举个基本的例子:

struct Foo
{
    std::vector<Bar> stuff;
};

在这种情况下,我们可以通过自定义内存分配器来轻松地分配Foo:

void* mem = custom_malloc(sizeof(Foo));
Foo* foo = new(foo_mem) Foo;
...
foo->~Foo();
custom_free(foo);

当然,我们可以把这一切都包装起来,以符合RAII,实现异常安全等。

但现在问题是一连串的。使用std::vectorstuff成员将希望使用std::allocator,现在我们有第二个问题需要解决。我们可以使用自己的分配器使用std::vector的模板实例化,如果您需要传递给分配器的运行时信息,您可以覆盖Foo的构造函数,将该信息与分配器一起传递给向量构造函数。

但是Bar呢?它的构造函数可能还想为各种不同的对象分配内存,因此问题会级联。

考虑到这个问题的困难,以及我尝试过的其他通用解决方案,以及移植时的悲伤,我决定采用一种完全不通用的、有点务实的方法。

我确定的解决方案是有效地重新设计整个C和C++标准库。我知道这很恶心,但在我的情况下,我有更多的借口这么做。我正在开发的产品实际上是一个引擎和软件开发工具包,旨在允许人们使用任何编译器、C运行时、C++标准库实现为其编写插件,并构建他们想要的设置。为了允许像向量、集合或映射这样的东西以ABI兼容的方式通过这些中心API,除了许多C标准函数外,还需要滚动我们自己的标准兼容容器。

这个devkit的整个实现围绕着这些分配函数:

EP_API void* ep_malloc(int lib_id, int size);
EP_API void ep_free(int lib_id, void* mem);

整个SDK都围绕着这两者,包括内存池和"子分配器"。

对于我们无法控制的第三方图书馆,我们只是SOL。其中一些库在内存管理方面也有同样雄心勃勃的事情,试图推翻这一点只会导致各种冲突,并打开各种蠕虫罐头。当使用像OGL这样的东西时,也有一些非常低级的驱动程序想要分配大量的系统内存,而我们对此无能为力

然而,我发现这个解决方案工作得很好,可以很快回答一个基本问题:"谁/什么在占用所有内存?"这个问题通常比与时钟周期相关的类似问题(我们可以启动任何探查器)更难回答。它只适用于我们控制下的代码,使用这个SDK,但我们可以在每个模块的基础上使用这个系统来获得非常彻底的内存分解。我们还可以对内存使用设置肤浅的上限,以确保内存不足的错误实际上得到了正确的处理,而不会试图耗尽系统中所有可用的连续页面。

因此,在我的案例中,这个问题是通过策略解决的:通过构建一个统一的编码标准和一个在整个代码库中使用的符合该标准的中央库(以及通过第三方为我们的系统编写插件)。这可能不是你想要的答案,但这最终是我们找到的最实用的解决方案。