【问题标题】:Why is vector::iterator invalidated upon reallocation?为什么 vector::iterator 在重新分配时无效?
【发布时间】:2012-06-13 23:22:02
【问题描述】:

我不明白为什么 vector 的迭代器应该在重新分配发生时失效。

难道不能简单地通过在迭代器中存储一个偏移量——而不是一个指针——来防止这种情况吗?

为什么vector 不是这样设计的?

【问题讨论】:

  • 每次插入除结尾或排序等之外的任何位置时,您都必须更改所有偏移量。
  • @JohnDibling:带指针的同上。
  • “你可以”和“值得做”是不同的东西。这是需求和 POV 的问题。
  • @curiousguy:如果不是为了性能,那当然值得,IMO。但由于性能问题,它不是。
  • @Mehrdad 我同意:“只为你使用的东西付费”

标签: c++ vector iterator invalidation


【解决方案1】:

TL;DR - 因为您正在用简单的无效规则换取更复杂的远距离操作。


请注意,“存储指向向量对象的指针”会导致新的失效情况。例如,今天swap 保留了迭代器的有效性,如果指向向量的指针(或引用)存储在迭代器中,则它不再可以。所有移动向量元数据本身的操作(向量的向量有人吗?)将使迭代器无效。

您的交易是“当对元素的指针/引用无效时迭代器变得无效”为“当对向量的指针/引用无效时迭代器变得无效”。

性能参数并不重要,因为建议的替代实现甚至都不正确。

【讨论】:

  • 是的,我真的没有这个问题了哈哈。我想我在问了这个问题后不久就意识到了这一点。不过谢谢! +1
【解决方案2】:

您可以通过包装标准 std::vector<T>::iterator 来增加安全性,但不能通过包装 extension::vector<T>::safe_iterator 来增加速度。这是一般原则,解释了许多 C++ 设计选择。

【讨论】:

    【解决方案3】:

    我的迭代器没有失效,它应该指向同一个元素还是在它之前的插入之后指向同一个位置?换句话说,即使没有性能问题,决定使用哪个替代定义也并非易事。

    【讨论】:

    • 问题是关于重新分配,而不是插入。前任push_back 可以触发重新分配。
    【解决方案4】:

    做出这些决定的原因有很多。正如其他人指出的那样,向量的iterator 的最基本实现是指向元素的普通指针。为了能够处理 push_back 迭代器,必须修改以处理指向向量和位置的指针,在通过运算符访问时,必须取消引用向量指针,获得指向数据的指针并添加位置, 额外取消引用。

    虽然这不是最有效的实施方式,但这并不是真正的限制因素。 VS/Dinkumware 库中(甚至在发行版中)迭代器的默认实现是经过检查的迭代器,它管理等量的信息。

    实际问题来自其他变异操作。考虑在矢量中间插入/擦除。为了保持所有迭代器的有效性,容器必须跟踪迭代器的所有实例并调整位置字段,以便它们仍然引用相同的元素(已被插入/删除替换)。

    【讨论】:

    • "调整位置字段,使它们仍然引用相同的元素(已被插入/删除移动)。" - 这只是“有效性”的一个概念 - 无论插入/删除如何,客户都可能想要引用第 N 个元素(接受确保仍然存在第 N 个元素的责任)。另外,即使使用“普通”迭代器,C++11 标准 23.3.6.5.1 和 23.4.6.5.3 也允许在插入或擦除点处或之后使迭代器/引用失效。
    • @TonyDelroy:迭代器引用容器中的元素。如果在获得迭代器后,迭代器不再引用将违反合同的相同元素。考虑迭代器上的运算符:auto it = std::find( v.begin(), v.end(), value ); v.erase( v.begin()+5 ); 此时(假设您的前提)不能保证 *it == value 是一个向量,但如果 v 是一个列表,那么它将得到保证。现在您已经将迭代器契约分解为不同的迭代器,具有完全不同的语义。
    • @TonyDelroy:关于报价,是的,没有人另有说法。一旦向量被修改,向量中的所有迭代器都将失效,无论迭代器指向哪里以及变异操作是什么。总结一下:如果您不 fix 迭代器指向的位置,则无法提供一致的迭代器语义(跨容器)。如果不跟踪所有迭代器,就无法修复迭代器。在不跟踪所有迭代器的情况下,任何向量上的变异操作都必须使所有迭代器无效。
    • @DavidRodríguez-dribeas:关于一致语义的好点(假设稍后在两个容器中都有值) - 这会令人困惑,但如果索引迭代器是,几乎肯定会采用第二种语义模型,并且有“优点”。 “没有跟踪...任何变异操作...使所有迭代器无效”-不,C++ 11 23.4.1.2.5/...4.3 及其他-从那时起擦除无效,清除和重新分配无效所有迭代器,没有其他修改器使迭代器无效(例如,不是不触发重新分配的插入,也不是早于擦除的元素)。 C++03 类似。
    • @TonyDelroy:确实如此。我倾向于站在安全的一边,并确保在说空话时我站在安全的一边。我应该对案件更准确。该标准基本上在所有引用元素 A 的指针或引用在操作后不再引用 A(重新分配、插入点之前的插入/删除)的所有情况下都调用迭代器无效。
    【解决方案5】:

    只是为了引用与性能相关的理由:在设计 C++ 时,Stroustrup 认为像 std::vector 这样的模板类接近原生数组的性能特征是至关重要的:

    强调运行时效率的一个原因......是我想要 模板在时间和空间上足够有效以用于 数组和列表等低级类型。

    ...

    更高级别的替代方案——例如,带有 size() 的范围检查数组 操作,多维数组,具有适当数值的向量类型 向量操作和复制语义等——将被 用户只有在他们的运行时间、空间和符号方便的情况下 接近那些内置数组。

    换句话说,提供参数化类型的语言机制 应该是这样的,相关用户应该能够负担得起 消除数组的使用,转而使用标准库类。

    Bjarne Stroustrup,C++ 的设计和演变,第 342 页。

    【讨论】:

      【解决方案6】:

      因为迭代器要做到这一点,他们需要存储一个指向向量对象的指针。对于每个数据访问,他们需要跟随指向向量的指针,然后跟随其中的指针指向数据数组的当前位置,然后添加偏移量 * 元素大小。这会慢得多,并且需要更多内存供size_type 成员使用。

      当然,有时这是一个很好的折衷方案,在需要时能够选择它会很好,但它比(C 风格)直接使用数组更慢、更笨重。在引入 STL 时,std::vector 的性能受到了无情的审查,并且正常的实现针对空间和速度进行了优化,超过了这个便利/可靠性因素,就像数组等效的 operator[] 与数组一样快,但安全性不如数组at().

      【讨论】:

      • +1 本来想接受这个,但 Nate 的回答被引用了! :\ 谢谢!
      • 有多少次我希望[] 是选中的访问权限,at 是未选中的访问权限:(
      • @Matthieu:同意!吹嘘你的代码有多快是件好事,但如果你使用at() 并节省分析的调试时间,你最终会更快...... :-)
      【解决方案7】:

      您需要同时存储偏移量和指向矢量对象本身的指针。

      按照规定,迭代器可以只是一个指针,占用空间更少。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-12-15
        • 2012-10-19
        • 2023-03-06
        • 2020-07-23
        • 2013-06-22
        • 1970-01-01
        相关资源
        最近更新 更多