std::vector *have* 在增加容量时移动对象吗?或者,分配器可以“重新分配”吗?

Posted

技术标签:

【中文标题】std::vector *have* 在增加容量时移动对象吗?或者,分配器可以“重新分配”吗?【英文标题】:Does std::vector *have* to move objects when growing capacity? Or, can allocators "reallocate"? 【发布时间】:2011-12-21 15:21:36 【问题描述】:

different question 激发了以下想法:

std::vector<T>在增加容量时移动所有元素吗?

据我了解,标准行为是底层分配器请求新大小的整个块,然后移动所有旧元素,然后销毁旧元素,然后释放旧内存。

考虑到标准分配器接口,这种行为似乎是唯一可能的正确解决方案。但我想知道,修改分配器以提供reallocate(std::size_t) 函数是否有意义,该函数将返回pair<pointer, bool> 并可以映射到底层realloc()?这样做的好处是,如果操作系统实际上可以扩展分配的内存,那么根本不需要移动。布尔值将指示内存是否已移动。

(std::realloc() 可能不是最好的选择,因为如果我们不能扩展,我们不需要复制数据。所以实际上我们宁愿想要像extend_or_malloc_new() 这样的东西。编辑: 也许基于is_pod-trait 的特化可以让我们使用实际的realloc,包括它的按位复制。只是一般情况下不会。)

这似乎是一个错失的机会。最坏的情况是,您始终可以将reallocate(size_t n) 实现为return make_pair(allocate(n), true);,这样就不会受到任何惩罚。

是否有任何问题导致此功能不适合或不适合 C++?

也许可以利用这一点的唯一容器是std::vector,但话说回来,这是一个相当有用的容器。


更新:一个小例子来澄清。当前resize()

pointer p = alloc.allocate(new_size);

for (size_t i = 0; i != old_size; ++i)

  alloc.construct(p + i, T(std::move(buf[i])))
  alloc.destroy(buf[i]);

for (size_t i = old_size; i < new_size; ++i)

  alloc.construct(p + i, T());


alloc.deallocate(buf);
buf = p;

新实现:

pair<pointer, bool> pp = alloc.reallocate(buf, new_size);

if (pp.second)  /* as before */ 
else            /* only construct new elements */ 

【问题讨论】:

我认为它不需要一对,您可以简单地与传入的指针进行比较。只要重新分配理解正确的移动语义我想不出问题。 @MooingDuck:关于您的第一条评论:唯一的可能是分配器的 grow 函数在无法grow,并让内存保持原样(没有按位复制)。当你开始比较 realloc 的指针时,损坏已经完成。 @David: grow 可以说是一个更好的功能名称! @Praetorian:按位复制有不同的问题......例如,考虑可能存在内部指针,例如我使用了 NullObject 模式的实现,其中对象持有一个 null-object 和一个指向当前对象的指针,它可以引用动态分配的 real-objectnull-object 成员.在对象为 null 的情况下,指针引用同一对象的另一个成员。在这种情况下,按位复制会留下悬空指针。 std::deque 是最不幸的容器之一。它真的很擅长它的工作。而且您几乎从不需要它的功能。对于 std::container,几何增长的循环缓冲区比 std::deque 更好。循环缓冲区具有更好的性能和更少的复杂性。但它不能保证像std::dequestd::list 这样的引用的稳定性。但根据我的经验,循环缓冲区比 std::deque 更好地解决了大多数 push-pop 队列问题,如果没有,std::list 是正确的选择。 【参考方案1】:

std::vector&lt;T&gt; 容量不足时,它必须分配一个新块。你已经正确地解释了原因。

IMO 它增加分配器接口是有意义的。我们两个人尝试过 C++11,但我们无法获得支持:[1][2]

我开始确信,为了完成这项工作,需要一个额外的 C 级 API。我也未能获得支持:[3]

【讨论】:

太好了,谢谢!很遗憾听到您未能获得进一步的支持——添加 C 接口函数似乎完全是微不足道的:只需使用 realloc() 并删除复制内存的部分。它甚至不必成为 C 标准的一部分。它可能只是 C++ 库实现的东西...... 自定义 C++ 分配器将受益于 C realloc-but-don't-move 函数。这是带着这个去 C 委员会的主要动机。 好吧,other,用户定义的类总是有可能使用具有这种“可能增长”接口的分配器。可惜被拒绝了。但是,编写一个比规定的分配器接口更多的分配器是否可以? 是的。但是std::containers 不够聪明,无法使用您的扩展界面。但也许 boost::intrusive 容器会。该库的作者与编写 N2045 的人是同一个人。 @gnzlbg - 你实际上不需要为每个mmap 维护一个指针来释放它。我猜你的假设是你需要将你从mmap(如newdelete)获得的相同指针传递回munmap,但它不起作用:你可以传递整个您要删除的页面范围,即使它们来自不同的mmap 调用,它们也会被释放。如果其中一些根本没有映射,这甚至不是错误!所以你只需要指向区域开始的指针(已经是向量的一部分)和容量(已经是向量的一部分)......【参考方案2】:

在大多数情况下,realloc 不会扩展内存,而是分配一个单独的块并移动内容。最初定义 C++ 时考虑到了这一点,并决定当前接口更简单,并且在常见情况下效率不低。

在现实生活中,realloc 能够成长的情况很少。在malloc 具有不同池大小的任何实现中,新大小(请记住vector 大小必须以几何方式增长)可能会落在不同的池中。即使在没有从任何内存池分配的大块的情况下,它也只能在更大大小的虚拟地址空闲时才能增长。

请注意,虽然realloc 有时可以增长内存而不移动,但当realloc 完成时它可能已经移动 (按位移动)内存,并且二进制 move 将导致所有非 POD 类型的未定义行为。我不知道任何分配器实现(POSIX、*NIX、Windows),您可以在其中询问系统是否能够增长,但如果它需要移动。

【讨论】:

我认为他的意思是一个新的重新分配功能,与 malloc.h realloc 不兼容,当无法调整大小时,它不会移动内容,也不会释放旧块。 另外,VirtualAllocmmap 允许您在块之后立即请求地址,因此您要么扩展块,要么失败。但我也不知道有任何基于堆的分配器支持“尽可能增长”操作。 @Ben:是的,确实如此。我需要 1) 为标准库分配器添加一个接口,以及 2) 一个底层实现,如果可能的话,它会增长,但如果没有,则执行裸 malloc,而不需要移动。 @BenVoigt:显然这样的功能肯定已经存在:我可以撕开realloc 并杀死按位移动的部分,不是吗? realloc++ :-) Linux 提供了mremap,您可以使用它来扩展现有的匿名内存映射(或尝试失败)。【参考方案3】:

是的,你说得对,标准分配器接口没有为 memcpy'able 类型提供优化。

可以使用 boost 类型特征库来确定一个类型是否可以被 memcpy 使用(不确定他们是开箱即用地提供它,还是必须基于 boost 构建一个复合类型鉴别器)。

无论如何,要利用realloc(),可能会创建一种可以显式利用此优化的新容器类型。使用当前的标准分配器接口,这似乎是不可能的。

【讨论】:

不,不,我不关心 memcpyability。我已经准备好调用我的分配器的构造和销毁序列。我只是想给分配器偷懒的机会!

以上是关于std::vector *have* 在增加容量时移动对象吗?或者,分配器可以“重新分配”吗?的主要内容,如果未能解决你的问题,请参考以下文章

如何将 std::vector 的容量限制为元素的数量

std::vector 向下调整大小

保留STD向量的容量 模板专业化

即使根据容量()仍有未使用的空间,std::vector 能否将其数据移动到 emplace_back()处的另一个地址?

std::vector 或 std::list 用于 std::unordered_map 存储桶?

vector