【问题标题】:Keeping std::list iterators valid through insertion通过插入保持 std::list 迭代器有效
【发布时间】:2011-09-07 23:46:33
【问题描述】:

注意:这不是我应该“使用列表还是双端队列”的问题。面对insert(),这是一个关于迭代器有效性的问题。


这可能是一个简单的问题,我只是太密集了,看不到正确的方法。我正在将网络流量缓冲区(无论好坏)实现为std::list<char> buf,并将我当前的读取位置保持为迭代器readpos

当我添加数据时,我会做类似的事情

buf.insert(buf.end(), newdata.begin(), newdata.end());

我现在的问题是,如何保持 readpos 迭代器有效?如果它指向旧 buf 的中间,那么它应该没问题(通过 std::list 的迭代器保证),但通常我可能已经读取并处理了所有数据并且我有 readpos == buf.end()。插入后,我希望readpos 总是 指向下一个未读字符,如果插入应该是第一个插入的字符。

有什么建议吗? (没有将缓冲区更改为std::deque<char>,这似乎更适合该任务,如下所示。)

更新:通过 GCC4.4 的快速测试,我观察到 deque 和 list 相对于 readpos = buf.end() 的行为不同:在末尾插入后,readpos 在列表中被破坏,但点到双端队列中的下一个元素。 这是标准保证吗?

(根据cplusplus,任何 deque::insert() invalidate 所有迭代器。这不好。也许使用计数器比迭代器更好地跟踪双端队列中的位置?)

【问题讨论】:

  • 使用双端队列,您将始终从头读取并在末尾插入...无需跟踪特定的读取位置。不,这不是标准保证;特别是,如果 dequeue 恰好重新分配,则前一个 end() 几乎肯定会引用已释放的内存。
  • 只是提到deque,您可以保留当前位置的数字索引而不是迭代器。
  • 最好不要依赖测试行为;它可以演示什么不起作用,但不保证任何看起来都有效。例如,如果存储没有重新分配,迭代器可能仍然有效,但您不能依赖它。
  • 谢谢大家——是的,我确实会简单地存储一个数字读取位置。尼莫:我不能总是在前面看书。我想我可以立即删除我从前面读取的数据,但有一些原因我可能只想在一些处理后删除。
  • 我认为你的问题标题应该是“保持 std::deque 迭代器通过插入有效”,因为std::list 迭代器通过任何插入。另外,如果您想知道是否应该使用listdeque,我说使用vector 和整数索引(只要数据大小相对较小)。顺便说一句,漂亮的dopefish。

标签: c++ iterator stdlist


【解决方案1】:

我确实很密集。该标准为我们提供了我们需要的所有工具。具体来说,序列容器要求 23.2.3/9 说:

a.insert(p, i, j) 返回的迭代器指向插入a 的第一个元素的副本,如果i == j,则指向p

接下来,list::insert 的描述是(23.3.5.4/1):

不影响迭代器和引用的有效性。

所以事实上,如果pos 是我当前正在使用的列表中的迭代器,我可以说:

auto it = buf.insert(buf.end(), newdata.begin(), newdata.end());

if (pos == buf.end()) { pos = it; }

我的列表中新元素的范围是[it, buf.end()),尚未处理的元素的范围是[pos, buf.end())。这是因为如果在插入之前pos 等于buf.end(),那么它仍然在插入之后,因为插入不会使 任何 个迭代器失效,甚至不会使结尾失效。

【讨论】:

    【解决方案2】:

    来自http://www.sgi.com/tech/stl/List.html

    “列表具有重要的属性,即插入和拼接不会使列表元素的迭代器失效,即使删除也会使指向被删除元素的迭代器失效。”

    因此,readpos 在插入后应该仍然有效。

    不过……

    std::list< char > 是解决这个问题的一种非常低效的方法。您存储在std::list 中的每个字节都需要一个指针来跟踪字节,加上列表节点结构的大小,通常需要两个指针。即至少 12 或 24 字节(32 或 64 位)的内存用于跟踪单个数据字节。

    std::deque< char> 可能是一个更好的容器。像std::vector 一样,它在后面提供恒定时间插入,但它也在前面提供恒定时间移除。最后,像 std::vector std::deque 是一个随机访问容器,因此您可以使用偏移量/索引而不是迭代器。这三个特点使它成为一个有效的选择。

    【讨论】:

    • 忽略关于双端队列的内容,如果readpos == buf.end(),readpos 将无效,因此插入后 readpos 不指向第一个新插入的元素。
    【解决方案3】:
    if (readpos == buf.begin())
    {
        buf.insert(buf.end(), newdata.begin(), newdata.end());
        readpos = buf.begin();
    }
    else
    {
        --readpos;
        buf.insert(buf.end(), newdata.begin(), newdata.end());
        ++readpos;
    }
    

    不优雅,但应该可以。

    【讨论】:

    • 如果 buf 为空,那不会爆炸吗? (我目前使用的 hack 基本上相当于你写的内容,并带有适当的空性检查。)
    • @Kerrek,如果 buf 为空,则 readpos==begin() 应该为真。我想begin() 在插入一个空列表后可能会发生变化(不确定)所以我会修复这种情况的代码。
    • 哦,好点子!这实际上可能是一种适度干净的方式。
    • 你说的是插入std::deque 对吧?因为插入std::list 不会使迭代器无效。
    • @bobobobo,但是end 会发生什么?即使它没有失效,它仍然指向容器的末尾还是现在指向新插入的项目?有了这段代码,就没有歧义了。
    【解决方案4】:

    list<char> 是一种非常低效的存储字符串的方式。它可能比字符串本身大 10-20 倍,而且您正在为每个字符追逐一个指针......

    您是否考虑过改用std::dequeue<char>

    [编辑]

    要回答您的实际问题,添加和删除元素不会使 list 中的迭代器无效...但 end() 仍将是 end()。因此,您需要在插入新元素时将其作为特殊情况进行检查,以更新您的 readpos 迭代器。

    【讨论】:

    • 正如我所说,无论好坏,这不是我的项目,我确信有更好的方法来做到这一点。双端队列可能很好——我担心从队列中间删除会很慢,但我相信我只需要从前面删除范围并在后面插入。双端队列对此有效吗?无论如何,这将是一个单一的 typedef 更改 :-) 但我的问题是面对 insert() 时的迭代器有效性!
    • @Kerrek SB:双端队列就是这样使用的(从极端中弹出和推送),所以我认为这将是您的最佳选择。至少比列表好
    • 是的,双端队列对于从开头或结尾插入和删除元素非常有效。 (肯定不在中间……)
    • 至于您的编辑:是的,我注意到 end() 无效,但问题是 如何获得正确的迭代器 到下一个元素。
    • 是的,我现在看到了问题。与单元素 insert 不同,范围 insert 不返回迭代器。 (这似乎是一个疏忽,但哦,好吧。)您可以推出自己的版本,或者只是坚持您当前的“hack”。
    猜你喜欢
    • 2020-11-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-13
    • 2018-02-13
    • 1970-01-01
    • 2020-12-02
    相关资源
    最近更新 更多