【问题标题】:C++ how to manage iterators of contiguous dynamic arraysC++如何管理连续动态数组的迭代器
【发布时间】:2018-02-02 17:21:45
【问题描述】:

我一直在尝试找出一种管理动态数组的有效方法,我可能会偶尔更改它,但希望经常随机访问和迭代。

我希望能够:

  • 将数组存储在一个连续的数据块中(减少缓存未命中)
  • 单独访问每个元素,独立于数组句柄(指针 > 索引)
  • 调整数组大小(动态)

因此,为了实现这一点,我一直在尝试使用 std::vector<T>::iterator,它工作得非常好,直到最近,当我调整了我存储迭代器的向量(例如调用 push_back())的大小时。所有的迭代器都失效了,因为它们指向的是陈旧的内存。

是否有任何有效的(可能是 STL-)方法来使迭代器指针保持最新?还是我必须手动更新每个迭代器?

整个方法是否值得?我应该坚持使用索引吗?

编辑: 我以前使用过索引,没关系,但我改变了我的方法,因为它仍然不好。我总是必须将整个数组拖到范围内,并且索引可以很容易地用于任何数组。也没有定义“NULL”索引的完美方法(我不知道)。

更新所有指针以及调整大小操作的选项怎么样?您所要做的就是存储原始vector::begin,调整vector 的大小,然后更新所有指向vector.begin() + (ptr - prevBegin) 的指针,调整大小操作已经是您应该尽量避免的事情了。

【问题讨论】:

  • 使用std::vector 并使用size_t 索引而不是迭代器。
  • @snow_abstraction 如果您还没有数组的起点,则索引是无用的,因此您需要两条信息。指针直接指向元素本身(但可以无效),因此您不需要任何其他内容。
  • @SteveLorimer:然后使用std::pair<std::vector<..> *, size_t>,甚至将其包装到支持operator*()/operator->()的类中
  • 您可以轻松地使用 -1 的索引来指示 NULL。无法更新指针,因为程序中没有指向数组的所有指针的主列表。
  • @ChrisDodd 如果他删除了一个项目,或者在向量后面以外的任何地方添加了一个项目,一些索引也将失效。不确定他是否需要那种功能

标签: c++ arrays pointers memory-management stl


【解决方案1】:

完全实现所有 3 个目标是不可能的。如果你是完全连续的,那么你就有一个大小有限的内存块,获得更多内存的唯一方法是请求更多内存,这与你已经拥有的内存不连续。所以你必须至少在某种程度上牺牲一个要求:

  • 如果您愿意部分牺牲连续性,您可以使用std::deque。这是一种数组数组类型的结构。它不会使引用无效,因为我认为任何增加其大小的操作。它取决于您的数据类型的详细信息,但通常它的性能更接近于连续数组而不是链表。做得好但旧(5 年)基准:https://baptiste-wicht.com/posts/2012/12/cpp-benchmark-vector-list-deque.html。另一种选择是编写一个分块分配器,与deque 或其他结构一起使用。不过,这需要更多的工作。
  • 如果你可以使用索引,那么你可以使用vector
  • 如果您不需要调整大小,您仍然可以只使用 vector 并且永远不要调整大小。

除非你有充分的理由,否则我会坚持使用索引。如果您的主要性能瓶颈与大量元素的迭代相关(正如您的连续性要求所暗示的那样),那么整个索引的事情应该真的不是问题。如果您确实有充分的理由避免使用索引(您没有说明),那么我将在主循环操作中对 dequevector 进行对比,以了解 deque 的实际效果有多差。情况可能会更糟,如果 dequevector 对您来说都不够好,那么下一个替代方案需要更多的工作(可能涉及分配器或自定义数据结构)。

【讨论】:

  • 接触和更新所有指向你的东西是困难的,你必须在向量附近的某个地方存储指向所有向量客户端的某种指针。让需要访问向量 v 的第 i 个元素的每个人都存储对 v 的引用和 i 本身是更合乎逻辑的。我不确定我是否看到了问题所在?您也可以将空索引设为 -1;这很可能会环绕到最大的无符号值 2^64-1,可能比您拥有的内存更多;-)。
【解决方案2】:

根据你的需要,是否可以使用如下数据结构:

std::vector<std::unique_ptr<Foo>>

那么无论向量如何调整大小,如果您通过Foo* 访问您的数据,指向 foo 的指针都不会失效。

随着您需要在向量中存储的Foos 的数量发生变化,向量可能需要调整其内部连续内存块的大小,这意味着您在向量内指向的任何迭代器都将失效 当向量调整大小时。

(您可以阅读更多关于 C++0x 迭代器失效规则的 here

但是,由于存储在向量中的对象是指向堆中其他位置的对象的指针,因此指向的对象(本例中为Foo)将不会失效

请注意,向量拥有 Foo(正如std::unique_ptr&lt;Foo&gt; 所暗示的那样),同时您可以通过保持非拥有指针来存储指向Foo Foo* 作为访问 Foo 数据的方法。

只要向量的寿命超过您通过Foo* 访问Foo 的需要,那么您就不会有任何寿命问题。

所以就您的要求而言:

  • 将数组存储在一个连续的数据块中(减少缓存未命中)

是的std::vector 做到了这一点

  • 单独访问每个元素,独立于数组句柄(指针 > 索引)

是的,存储 Foo* 作为您单独访问每个元素的方法,并且它保持独立于数组句柄 (vector::iterator)

  • 调整数组大小(动态)

是的std::vector 会自动实现这一点,并在您需要时为您调整大小。

奖励:

在向量中使用智能指针(在本例中为std::unique_ptr)意味着内存管理也会自动为您处理。 (只要确保你在向量被销毁之后不要尝试访问Foo*

编辑:

在 cmets 中已经指出,在向量中存储 std::unique_ptr&lt;Foo&gt; 违反了将对象存储在连续内存中的要求(如果这确实是您的意思 将数组存储在连续内存中) ,因为向量的底层数组是连续的,但访问Foo 对象将导致间接)。

但是,如果您对 vectorFoo 对象都使用合适的分配器(例如 arena 分配器),那么您将有更高的机会遭受更少的缓存未命中,因为您的 Foo 对象将存在靠近 vector 使用的内存,因此在遍历向量时更有可能在缓存中。

【讨论】:

  • 坦率地说,这不应该得到支持。这里的实际数据不是连续存储的,迭代这个向量可能会导致大量缓存未命中,这与问题的要求相矛盾。
  • @NirFriedman 很公平 - 我添加了一条关于使用合适的分配器将对象 near 到向量缓冲区的评论
  • 我同意弗里德曼,所有Foos的数据是如何连续存储的?指向每个Foo 的指针可能是,但除非我将所有Foos 存储在vector 中,否则它们可以分配到任何地方。但是当使用std::vector 存储所有Foos 时,在调整vector&lt;Foo&gt; 的大小时,我仍然会使所有unique_ptr&lt;Foo&gt;s 指针无效
  • @stimulate 你不能同时满足所有 3 个要求 - 但通过使用分配器你可以接近
  • @SteveLorimer 您显然可以使用分配器,但实际上您只是在转移问题,一旦您编写了一个好的分配器,您也可以将它与链表一起使用,其中 unique_ptr 的向量是与deque 非常相似或其他数据结构。 unique_ptr 建议本身的向量(您的答案所关注的)并没有给您带来太多收益,事实上,通过我能想到的链接列表,您什么也买不到。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-02-18
  • 1970-01-01
  • 2010-09-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多