调用erase时STL迭代器失效的问题

时间:2010-12-03 03:52:39

标签: c++ iterator deque erase invalidation

STL标准定义当在诸如std :: deque,std :: list等容器上发生擦除时,迭代器将失效。

我的问题如下,假设std :: deque中包含的整数列表,以及一对指示std :: deque中元素范围的指示,删除所有偶数元素的正确方法是什么?

到目前为止,我有以下内容,但问题是假设结束在擦除后无效:

#include <cstddef>
#include <deque>

int main()
{
   std::deque<int> deq;
   for (int i = 0; i < 100; deq.push_back(i++));

   // range, 11th to 51st element
   std::pair<std::size_t,std::size_t> r(10,50);

   std::deque<int>::iterator it = deq.begin() + r.first;
   std::deque<int>::iterator end = deq.begin() + r.second;

   while (it != end)
   {
      if (*it % 2 == 0)
      {
         it = deq.erase(it);
      }
      else
        ++it;
   }

   return 0;
}

检查如何实现std :: remove_if,似乎有一个非常昂贵的复制/降档过程正在进行中。

  • 在没有所有复制/转移的情况下,是否有更有效的方法来实现上述目标

  • 一般情况下,删除/删除元素比使用序列中的下一个第n个值进行交换更昂贵(其中n是到目前为止删除/删除的元素数)

注意:答案应该假设序列大小非常大,+ 1mil元素,并且平均1/3的元素将被删除。

4 个答案:

答案 0 :(得分:8)

我会使用Erase-Remove Idiom。我认为链接的维基百科文章甚至可以显示你正在做的事情 - 删除奇怪的元素。

remove_if所做的复制并不比从容器中间删除元素时的复制成本高。它甚至可能更有效率。

答案 1 :(得分:5)

调用.erase()也会导致“正在进行非常昂贵的复制/降档过程。”从容器中间擦除元素时,该点之后的每个其他元素必须向下移动一个点到可用空间。如果擦除多个元素,则会为每个已擦除元素产生成本。一些未擦除的元素将移动几个点,但是被迫一次移动一个点而不是一次移动一个点。这是非常低效的。

标准库算法std::removestd::remove_if优化了这项工作。他们使用一个聪明的技巧来确保每个移动的元素只移动一次。这比你自己做的要强得多<强> ,与你的直觉相反。

伪代码是这样的:

read_location <- beginning of range.
write_location <- beginning of range.
while read_location != end of range:
    if the element at read_location should be kept in the container:
        copy the element at the read_location to the write_location.
        increment the write_location.
    increment the read_location.

正如您所看到的,原始序列中的每个元素只被认为是一次,如果需要保留它,它只会被复制一次到当前的write_location。永远不会再看一遍,因为write_location永远不会在read_location前面运行。

答案 2 :(得分:2)

请记住,deque是一个连续的内存容器(如vector,可能是共享实现),因此删除容器中的元素必然意味着在孔上复制后续元素。您只是想确保进行一次迭代并将每个不要删除的对象直接复制到其最终位置,而不是在每次删除期间逐个移动所有对象。 remove_if在这方面是有效和适当的,erase循环不是:它会进行大量不必要的复制。

FWIW - 替代方案:

  • 在您的对象中添加“已删除”状态并将其标记为已删除,但是每次操作容器时都需要自行检查
  • 使用一个列表,它是使用指向前一个和下一个元素的指针实现的,这样删除一个列表元素会改变相邻的点以绕过该元素:没有复制,有效的迭代,只是没有随机访问,更小(即低效)堆分配和指针开销

选择什么取决于特定操作的性质,相对频率和性能要求(例如,如果它们在非关键时刻完成,但需要尽可能快的迭代,则可以提供缓慢的移除 - 无论如何确保您了解自己的需求以及各种数据结构的含义。

答案 3 :(得分:0)

未提及的一个替代方案是创建新的deque,复制要保留在其中的元素,并swap使用旧的deque。< / p>

void filter(std::deque<int>& in, std::pair<std::size_t,std::size_t> range) {
    std::deque<int> out;
    std::deque<int>::const_iterator first = in.begin();
    std::deque<int>::const_iterator curr = first + range.first;
    std::deque<int>::const_iterator last = first + range.second;
    out.reserve(in.size() - (range.second-range.first));
    std::copy(first, curr, std::back_inserter(out));
    while (curr != last) {
        if (*curr & 1) {
            out.push_back(*curr);
        }
        ++curr;
    }
    std::copy(last, in.end(), std::back_inserter(out));
    in.swap(out);
}

我不确定你是否有足够的内存来创建副本,但通常更快更容易制作副本而不是尝试从大型集合中内联擦除元素。如果你仍然看到内存颠簸,那么通过调用std::count_if来确定要保留的元素数量并保留那么多元素。这样你就可以分配一个内存。