为啥 C++ 分配器中没有重新分配功能?

Posted

技术标签:

【中文标题】为啥 C++ 分配器中没有重新分配功能?【英文标题】:Why is there no reallocation functionality in C++ allocators?为什么 C++ 分配器中没有重新分配功能? 【发布时间】:2011-03-07 12:11:53 【问题描述】:

在 C 中,标准内存处理函数是 malloc()realloc()free()。但是,C++ stdlib 分配器只并行其中两个:没有重新分配功能。当然,不可能与realloc() 完全相同,因为简单地复制内存不适用于非聚合类型。但是,比如说,这个函数会不会有问题:

bool reallocate (pointer ptr, size_type num_now, size_type num_requested);

在哪里

ptr 以前使用相同的分配器分配给 num_now 对象; num_requested >= num_now;

和语义如下:

如果分配器可以将ptr 处的给定内存块从num_now 对象的大小扩展到num_requested 对象,它会这样做(使额外的内存未初始化)并返回true; 否则它什么都不做并返回false

当然,这不是很简单,但据我所知,分配器主要用于容器,而容器的代码通常已经很复杂了。

假设有这样一个函数,std::vector 可以增长如下(伪代码):

if (allocator.reallocate (buffer, capacity, new_capacity))
  capacity = new_capacity;     // That's all we need to do
else
  ...   // Do the standard reallocation by using a different buffer,
        // copying data and freeing the current one

不能完全改变内存大小的分配器可以通过无条件的return false;来实现这样的功能。

是否有这么少的可重新分配的分配器实现不值得费心?还是我忽略了一些问题?

【问题讨论】:

+1,这个问题一直困扰着我。 Stroustrup 对这件事的看法:www2.research.att.com/~bs/bs_faq2.html#renew;它将问题委托给向量的内部工作,但没有说明为什么没有像“更新”这样的机制来简化数组的增长。 在某些情况下没有什么能阻止std::vector 这样做(例如,它知道它使用标准分配器)。允许标准库使用底层系统的知识。 【参考方案1】:

来自: http://www.sgi.com/tech/stl/alloc.html

这可能是最有问题的 设计决策。它会有 可能对 提供一个重新分配的版本 要么改变了 没有复制的现有对象或 返回 NULL。这本来可以的 直接对带有副本的对象有用 构造函数。它也会有 避免不必要的复制 其中原始对象没有 已完全填写。

不幸的是,这会 禁止从 C 中使用 realloc 图书馆。这反过来又会增加 许多分配器的复杂性 实现,并且会做出 与内存调试的交互 工具更难。于是我们决定 反对这种选择。

【讨论】:

我认为他们至少没有向分配器的接口添加重新分配方法是一种耻辱。它本可以被实现为仅释放现有块并分配一个新块,但它可能会在以后实现一个新块,而无需使用 Allocator 接口重做所有代码。 遗憾的是,C 从未费心添加“如果方便则调整分配大小”功能,但前提是允许符合要求的实现始终决定调整大小是“不方便的” @supercat “如果方便,调整分配大小”的行为与 realloc() 的行为有何不同? @martinkunev:指向现有分配的指针将保持有效。这在代码能够计算所需缓冲区大小的上限、分配那么多空间、将数据写入该缓冲区,然后知道实际需要保留多少缓冲区的情况下尤其有用--释放它不需要的缓冲区部分。 @martinkunev:此外,如果 realloc 有一个选项来指示新大小是否可能是分配的“最终”大小,它可以使用它在最适合和最差之间进行选择-fit 分配,后者提供了更大的可能性来扩展分配而无需重新定位它。【参考方案2】:

这实际上是 Alexandrescu 指出的标准分配器的设计缺陷(不是 operator new[]/delete[],而是最初用于实现 std::vector 的 stl 分配器,例如)。

realloc 的发生速度明显快于 malloc、memcpy 和 free。但是,虽然可以调整实际内存块的大小,但它也可以将内存移动到新位置。在后一种情况下,如果内存块由非 POD 组成,则所有对象都需要在 realloc 之后销毁和复制构造。

标准库需要将其作为一种可能性的主要内容是作为标准分配器公共接口的一部分的重新分配函数。即使默认实现是 malloc 新大小的块并释放旧的块,像 std::vector 这样的类当然可以使用它。它需要是一个能够销毁和复制构建内存中对象的函数,但如果这样做,它就不能以不透明的方式处理内存。这涉及到一些复杂性,并且需要更多的模板工作,这可能是标准库中省略它的原因。

std::vector<...>::reserve 是不够的:它解决了可以预期容器大小的不同情况。对于真正可变大小的列表,realloc 解决方案可以使像 std::vector 这样的连续容器更快,特别是如果它可以处理内存块成功调整大小而不被移动的 realloc 情况,在这种情况下它可以省略调用 copy内存中对象的构造函数和析构函数。

【讨论】:

std::vector 理论上可以专门用于可复制的对象并使用普通的realloc,只要new 没有被非默认版本替换...检测到可能只可能在链接时,而不是编译时,所以 gcc/clang / 等不要这样做。【参考方案3】:

您所要求的本质上就是vector::reserve 所做的。如果没有对象的移动语义,就无法在不进行复制和销毁的情况下重新分配内存和移动对象。

【讨论】:

这种功能的一个很好的用例是稀疏容器。在它们中使用向量,尤其是使用预分配内存时,将完全违背它们的目的(稀疏意味着节省内存)。 @doublep:如果您想要稀疏容器,那么(动态)分配的数组和向量都不是您想要的。 @Martin York:例如,Google Sparsehash 库使用动态分配的数组并取得了非常好的结果。 @doublep:我确信它确实使用了动态分配的数组。但它不使用“A”动态分配的数组,因为它比这更复杂。 我认为它不像vector::reserve,除非你当然愿意生活在循环世界中,分配器在vector之上实现,而vector又在分配器... 是的,对于最终用户,向量保留有点像realloc:向量的容量增长了一定量。当然,在内部,vector 只是在分配器函数之上实现,只能分配一个全新的块并释放一个完整的块:它不能要求扩展现有块。【参考方案4】:

我想这是上帝出错的地方之一,但我懒得写信给标准委员会。

应该有一个用于数组分配的重新分配:

p = renew(p) [128];

或类似的东西。

【讨论】:

如果你使用向量而不是数组,那就是.reserve()。当向量通常更好时,为什么要为数组添加新功能? @DavidThornley,引擎盖下的向量必须通过分配器接口。因此,向量似乎无法尽可能有效地释放未使用的内存。 (但我认为/希望我在这里遗漏了一些东西!) @DavidThornley - 正是亚伦所说的。 每个人都说“使用vector”——但他们说的是错误的抽象级别! vector 本身必须建立在(实际上是建立在)较低级别的分配例程上,这些例程可以让您获得未初始化的内存等等。如果这些例程不提供低级别的“重新分配”功能,那么vector 肯定也不能。当然,它可以提供resizereserver 以及其他所有功能,但在幕后,它们只是分配新块并复制内容。没有什么比扩展现有块更重要了。【参考方案5】:

由于 C++ 面向对象的特性,以及包含各种标准容器类型,我认为与 C 语言相比,对方向内存管理的关注较少。我同意在某些情况下 realloc()会很有用,但解决这个问题的压力很小,因为几乎所有生成的功能都可以通过使用容器来获得。

【讨论】:

我不确定我是否同意他们因为 OOP 而较少考虑它。 Placement new 是专门用于对象内存管理的功能示例。 我并不是说他们考虑得更少,只是实际语法的设计是为了在两者是不同选项的情况下更加强调 OOP 而不是直接内存管理。 Placement new 是程序员一起使用这两者而不是一个替换另一个的完美示例。 实际上placement new 是启用 C++ 工具中非常困难的自定义内存管理(特别是内存池)的设备。我认为它是一种鼓励使用 c++ 代码库自定义内存管理的设备。 您不能使用容器来获得realloc 的功能(至少如果您将性能包含在“功能”中),因为容器是在之上实现的低级内存分配例程,例如 std::allocator,最终调用甚至更低级别的 C++ 库方法,例如 operator new(size t),然后(通常)又调用 C 级 malloc 或其他。由于用于分配的较低级别的 C++ 抽象不包括重新分配,因此较高级别的容器将无法凭空合成它。

以上是关于为啥 C++ 分配器中没有重新分配功能?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 spark 没有在多个节点上重新分配我的数据帧?

为啥允许将字符串文字分配给 C++ 中 char * 类型的指针 [重复]

为啥在 c++ 中分配 char 数组元素时,分配的字符被破坏?

为啥不能重新分配var?

我用的DHCP分配一个单位的网络ip,但是我重启计算机后为啥还是这个ip啊

为啥我的编译器在 c++ 中使用动态分配的内存时给我错误