【问题标题】:Why doesn't the C++ standard library have a version of deque with contiguous memory?为什么 C++ 标准库没有具有连续内存的双端队列版本?
【发布时间】:2015-10-16 07:58:15
【问题描述】:

据我了解,deque 会根据需要分配新的恒定大小的内存块,但不能保证它们是连续的。这确保了从任一端移除不会使当前指向双端队列的任何迭代器无效。

这种内存布局在某些情况下很方便,但通常比连续存储内存要慢。

用于更快的连续内存的首选容器是向量,但它不允许您从前面推送或弹出。

我不明白为什么会这样。

我确信可以实现使用连续内存的双端队列,而且它似乎严格优于向量。它的内存布局将同样快,并且还允许从前面高效推送/弹出。

不仅如此,我觉得从设计的角度来看它也会更有意义。连续双端队列将是其速度的首选,当它的内存布局更适合手头的问题时,将使用非连续双端队列。

如果我有什么遗漏或近视,请告诉我。为什么标准库中没有连续的双端队列?

【问题讨论】:

  • @gha.st: 从什么时候开始?
  • @YvesDaoust 因为它会换行,因此使用了两个内存区域?
  • @gha.st:这是一个单一的、连续的区域。
  • @YvesDaoust 取决于您的定义。但是您可以vector<T> 的全部内容发送到期望T* 的函数中。这对于环形缓冲区是不可能的。
  • @djscrew:缺乏需求/证据表明它经常显着有益,想要实现它的人会满足于一个通用的实现,没有人愿意提出它。你可以编写一个实现并自己提出你知道的,甚至提出它来提升,如果它在那里流行和健壮,那么稍后的某个时候标准。

标签: c++ c++11 data-structures stl


【解决方案1】:

标准库中的容器并不意味着涵盖所有可能的用例,它们只是一些有用的数据结构,具有(大部分)不重叠的属性(如果您将四个关联容器算作一个系列,并且四个无序容器作为另一个系列)。

对于连续内存,使用向量。为了高效拼接,使用列表。为了在任一端实现高效的插入和删除无需大量重新分配和改组,请使用双端队列等。

如果你想要一个不同的数据结构然后写它,这就是 STL 设计的重点:任何其他遵循相同原则的容器都可以完美地与 STL 风格的算法一起工作。

你提出的结构会比向量有更多的开销,需要四个指针而不是三个。它可能需要前端容量和后端容量成员函数,因此您可以知道在任一端插入是否会重新分配和/或使迭代器无效。当它在一端的空间用完时,您将不得不决定是重新分配还是将所有内容随机排列到另一端的可用空间中。根据使用模式,做出错误的决定可能代价高昂(例如,重新分配并在两端有更多增长空间可能会更好,但这可能会导致一端浪费大量空间,并且如果您使用 shuffle 而不是重新分配您可能只是延迟了不可避免的事情,并且无论如何都必须在以后重新分配)。

但这些都不是无法解决的问题,所以原则上没有什么能阻止你编写这样的容器。但这并不意味着它必须在标准库中。

标准库也没有像 Boost.Container 的 flat_map 和 stable_vector 这样的容器,但您可以在第三方库中找到它们。同样,可扩展性是 STL 设计的重点。

【讨论】:

    【解决方案2】:

    它已经存在。您可以在向量的前面插入。

    是的,当然,它使引用和迭代器无效,并且需要复制(或移动)向量的内容,但如果你想要连续的内存,这是你必须做出的权衡。

    双端队列为您提供的是保证在任一端插入不会使对现有元素的引用无效。这只是可能的,因为它使用连续内存。

    【讨论】:

    • @djscrew,这需要您提前确定最大元素数量,并预先分配所有内存。
    • 不一定。一旦您拥有的内存块被填满,您可以将其全部重写为更大的内存块。增长与向量的工作方式相同,但它会保留从前面有效推送和弹出的能力
    • @djscrew:实现它,放到 GitHub 上,提交到 Boost Library Incubator。
    • @djscrew:在上面的大纲中,竞技场按需增长,你最终得到一个容器,在该容器上,迭代器在插入时无效(不是所有插入,而是那些增加缓冲区的插入),这是双端队列的主要优势。如果您只想避免分段分配中的潜在缺陷(在 VS 的情况下接近列表),那么它可以在向量之上轻松实现。我不认为这是一个通用容器,并且怀疑它应该在标准库中。
    • @djscrew 当然,但它不像双端队列,因为双端队列提供的保证是引用保持有效或 所有 插入到前面或后面。您的提案只能达到一些预先保留的限制。我并不是说你的提议不能工作或有用(并且可能作为一个围绕向量的相当简单的适配器类),但它不会像双端队列那样。
    【解决方案3】:

    我想,几乎不需要这样的容器,因为如果超出分配的内存,对连续内存的惩罚将是迭代器失效和重新分配(包括复制/移动元素)。

    deque 支持以 O(1) 的成本向任一端推送而不使现有迭代器无效、重新分配、复制或移动元素。这显然被认为比连续内存更重要。

    vector 实现了一个可增长的连续内存容器,并通过三个数据成员实现,对应于beginendend_memory

    P-------------E----------C   linear memory
    ^             ^          ^
    begin         end        end_memory
    

    所以只要end<end_memory 你可以push_back (=++end) 并且你总是可以pop_back (=--end)。最初是end=end_memory=begin+size,因此如果您从不推送或弹出,则不会浪费内存并且不需要重新分配(如果您使用reserve(),那么end_memory=begin+capacity 允许您在不重新分配的情况下执行一些push_backs )。

    为了拥有连续的内存并能够高效地push_front,您需要四个数据成员:

    A------------B--------E---------C   // linear memory
    ^            ^        ^         ^
    begin_memory begin    end       end_memory
    

    push_front 实现为--begin。但是,在构造新容器时,begin 应该是什么并没有明显的方法。你可以使用默认的begin_memory=beginend=end_memory=begin+size,但是你会有一个更复杂的reserve

    reserve(size_t capacity_to_push_front,
            size_t capacity_to_push_back);
    

    因此,如果您真的需要,请自行实施。

    【讨论】:

    • 所以本质上更高的复杂性超过了从前面推送/弹出的有用性。谢谢,这就是我正在寻找的答案。
    猜你喜欢
    • 1970-01-01
    • 2014-06-28
    • 2018-12-05
    • 2014-12-08
    • 2019-04-13
    • 1970-01-01
    • 2020-04-19
    • 2021-11-10
    • 1970-01-01
    相关资源
    最近更新 更多