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

37

C 语言中的标准内存处理函数是 malloc()realloc()free()。然而,C++ 的 stdlib 分配器只并行支持其中两个函数:没有重新分配函数。当然,这并不可能完全像 realloc() 那样做,因为简单地复制内存对于非聚合类型来说是不适当的。但是,使用下面的函数会有问题吗:

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

函数的含义如下:

  • 如果分配器能够将已分配给 num_now 个对象的内存块在 ptr 处从大小扩展到 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;的函数来解决。

是否很少有能够重新分配内存的分配器实现,以至于不值得费心去解决?还是我忽略了一些问题?


17
+1,这是一个一直困扰我的问题。 - Matteo Italia
Stroustrup 对这个事情的看法:http://www2.research.att.com/~bs/bs_faq2.html#renew;它将问题委托给 vector 的内部机制,但并没有说明为什么没有像 "renew" 这样的机制来使数组的增长更简单。 - Matteo Italia
3
在某些情况下,std::vector 可能会采用这种方式(例如,它知道自己在使用标准分配器),没有任何障碍可以阻止标准库利用底层系统的知识。 - KeithB
5个回答

19

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

这可能是最值得质疑的设计决定之一。更有用的做法可能是提供一个reallocate的版本,它可以在不复制现有对象的情况下改变其大小或返回NULL。这将直接为具有拷贝构造函数的对象提供帮助。此外,在原始对象未完全填充的情况下,也避免了不必要的复制。

不幸的是,这将禁止使用来自C库的realloc。这反过来会增加许多分配器实现的复杂性,并使与内存调试工具的交互更加困难。因此,我们否决了这个替代方案。


4
我认为他们没有在分配器接口中至少添加一个重新分配方法是一件遗憾的事情。该方法可以实现释放现有块并分配新块,但这将为以后实现新方法提供潜力,而无需重做使用分配器接口的所有代码。 - stinky472
3
C语言没有添加“方便时调整内存分配大小”的功能,这也是遗憾之处。如果加入了这一功能,需要注明符合规范的实现可以始终决定不调整大小。请注意,本翻译旨在使内容更加通俗易懂,但不改变原意。 - supercat
1
@martinkunev:对现有分配的指针仍然有效。这在代码能够计算所需缓冲区大小的上限、分配那么多空间、向该缓冲区写入数据,然后——知道它实际上需要保留缓冲区的多少部分之后——释放它不需要的部分的情况下特别有用。 - supercat
1
此外,如果realloc有一个选项可以指示新大小可能是分配的“最终”大小,它可以使用该选项在最佳适配和最差适配之间进行选择,后者提供了更大的扩展分配而无需重新定位的可能性。 - supercat
@martinkunev:关于保留地址的收缩,这可能对某些情况有用,但会禁止一些本来可以提高性能的优化(例如,如果realloc传递一个被空间包围的块,则将该块重新定位到该空间的开头即使不扩大也可能有所帮助)。不幸的是,免费编译器维护者专注于虚假的基于ub的“优化”,同时没有努力添加可以轻松提高效率的库函数。 - supercat
显示剩余3条评论

15

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

realloc可以比malloc,memcpy和free更快地发生。然而,在实际内存块大小可调整的情况下,内存块也可以移动到新位置。在后一种情况下,如果内存块由非POD组成,则所有对象在重新分配后都需要进行销毁和复制构造。

标准库需要适应此类情况的一个“可能性”就是将reallocate函数作为标准分配器公共接口的一部分。像std::vector这样的类肯定会用到它,即使默认实现是malloc新大小的块并释放旧块。然而,它需要是一个能够销毁和复制构造内存中对象的函数,而不能以不透明方式处理内存。这里涉及到一些复杂性,需要一些更多的模板工作,这可能是为什么它被省略在标准库中的原因。

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


1
理论上,std::vector 可以为平凡可复制的对象进行特化,并使用普通的 realloc,只要 new 没有被替换为非默认版本...检测这一点可能只能在链接时而不是编译时进行,因此 gcc/clang / 等不会这样做。 - Peter Cordes

8
你所要求的基本上就是vector::reserve所做的。如果没有对象的移动语义,就无法重新分配内存并移动对象而不进行复制和销毁。

1
@doublep:如果你想要稀疏容器,既不是(动态)分配的数组也不是向量。 - Martin York
@doublep:我确信它使用了动态分配数组。但它并没有使用“A”作为动态分配数组,因为它比那更加复杂。 - Martin York
@Mark Ransom:如果您有一个类似于vector的类(使用放置new/手动销毁,除了malloc分配的内存),在支持存储器上调用realloc会有任何问题吗?或者使用new创建一个调整大小的后备并将现有对象复制到其中。只要您不访问后备内存的两个副本,浅拷贝就可以正常工作,对吗? - Hayman
1
我认为它与vector::reserve完全不同,除非你愿意生活在分配器实现在vector之上,而vector又是在分配器之上实现的循环世界中... 是的,对于最终用户来说,vector reserve确实有点像realloc:向量的容量增加了一定量。当然,在内部,vector只是在分配器函数之上简单地实现,并且只能分配一个全新的块并释放一个完整的块:它不能要求扩展现有块。 - BeeOnRope
因此,向量在增加容量时会复制其所有元素,而乐观情况是成功请求扩展现有块且不需要复制。在填充向量的情况下,这可以避免大量复制和碎片化。许多其他容器也可以使用它。因此,您无法真正比较vector::resize和假设的allocator::reallocate,因为后者是用于实现后者的工具。 - BeeOnRope
显示剩余3条评论

2

我想这可能是上帝做错了的事情之一,但我只是太懒了,没有写给标准委员会。

数组分配应该有一个重新分配的功能:

p = renew(p) [128];

或者类似这样的东西。


如果您使用向量而不是数组,则有.reserve()。既然向量通常更好,为什么要为数组添加新功能呢? - David Thornley
1
@DavidThornley,向量在底层必须通过分配器接口进行操作。因此,似乎向量无法尽可能高效地释放未使用的内存。(但我认为/希望我在这里漏掉了什么!) - Aaron McDaid
2
@DavidThornley - 正如Aaron所说的那样。_每个人_都说“使用vector”-但他们在错误的抽象级别上谈论! vector本身必须建立在(事实上是建立在)允许您获取未初始化内存等低级分配例程之上。如果这些例程不提供低级“重新分配”函数,那么vector肯定也不能提供。当然,它可以提供resizereserver以及其他所有内容,但在底层,这些只是分配新块并复制内容。没有像扩展现有块这样的东西。 - BeeOnRope

2

由于C++具有面向对象的特性,并且包含各种标准容器类型,我认为相对于C语言,更少地关注了直接内存管理。我同意realloc()在某些情况下可能会有用,但是解决这个问题的压力很小,因为几乎所有的功能都可以通过使用容器来实现。


1
我不确定我同意他们因为面向对象编程的原因而考虑得更少。放置 new 是一个专门用于对象内存管理的特性示例。 - Joseph Garvin
我并不是说他们对此考虑得更少,只是实际的语法设计旨在在两者不同选项之间更加强调面向对象编程而不是直接内存管理。放置新对象是一个完美的例子,程序员可以将两者结合使用,而不是一个替换另一个。 - tlayton
实际上,放置新对象(placement new)是一种工具,它可以在C++设施内启用本来非常困难的自定义内存管理(特别是内存池)。我认为它是一种鼓励使用C++代码库进行自定义内存管理的设备。 - Ofek Shilon
2
如果你把性能算在“功能”范畴内,那么你不能使用容器来获得realloc的功能。因为容器是基于低级别的内存分配例程实现的,例如std::allocator,这些例程最终调用更低级别的C++库方法,例如operator new(size t),然后再转而(通常)调用C级别的malloc或其他方法。由于低级别的C++分配抽象不包括重新分配,所以高级别的容器无法从空气中合成它。 - BeeOnRope

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接