【问题标题】:Iterating through STL containers and removing/adding multiple items遍历 STL 容器并删除/添加多个项目
【发布时间】:2013-06-21 08:30:06
【问题描述】:

我的代码中最常见的错误之一是 STL 容器在循环期间被修改。

元素在循环执行期间被删除或添加,所以我通常会遇到越界异常。

我的 for 循环通常如下所示:

for (auto& Item : Items) { // Will not work when Items container is modified
    //... loop logic
}

当可以删除多个项目时,我使用这个怪物:

for (int Index=Items.size()-1;Index<=0;Index--) {
    if (Index<Items.size()) { //Because multiple items can be removed in a single loop
        //... loop logic
    }
}

这看起来很糟糕,使用第二个选项让我感觉很糟糕。可以删除多个项目的原因是由于事件,其中单个事件可以删除任意数量的元素。

这里有一些伪代码来说明何时发生这种情况:

// for each button in vector<button> {
// process button events
// event adds more buttons to vector<button>
// *ERROR* vector<button> is modified during loop.
// }

在另一个例子中,想象一个包含以下项目的向量:

// 0 1 2 3 4 5 6 7 8 9

我们从0 开始循环,逐个元素地进行。在4,我想删除元素149,所以我们不能在这里使用普通循环。

【问题讨论】:

  • 您可以在删除或添加元素时简单地修改迭代器 - 只要您知道修改了多少项以及在哪里修改。
  • @Spook 我通常不知道什么和在哪里,一个事件可以在 for 循环期间随时删除任意数量的元素。
  • 好吧,如果你真的不知道何时何地可以删除元素(以及多少),你唯一的选择是复制容器并修改它......否则没有让这个工作的方法。
  • 不过,这引出了一个问题,如果按下按钮 4 删除按钮 9,而您实际上删除了它,然后按下按钮 5 应该删除按钮 9... 它是相同的“9”或确实如此删除以前的第 10 个元素?换句话说,您的元素是由键标识还是由它们在容器中的位置标识?

标签: c++ c++11


【解决方案1】:

std::remove_if 与决定是否需要删除按钮的谓词一起使用:

bool needsRemoved(const Button& button);

vec.erase(std::remove_if(vec.begin(), vec.end(), &needsRemoved), vec.end());

编辑:对于您的最后一个示例,二次(即对性能不利)算法是:

std::vector<int> vec = {0,1,2,3,4,5,6,7,8,9};
auto end = vec.end();
for (auto it = vec.begin(); it < end; ++it)
{
    std::set<int> bad = {1, 4, 9};
    end = std::remove_if
        (vec.begin(), end,
         [bad](int x) { return (bad.find(x) != bad.end()); });
}
vec.erase(end, vec.end());

不过,使用快速查找的容器(如集合或地图)可能会更好。

【讨论】:

  • 这如何解决他的问题。他的问题是按钮 4 中的某些内容可能会删除按钮 1、4 和 9。使循环中的迭代器无效。
  • @James Kanze,我省略了最后一部分,请参阅编辑后的答案,感谢您指出。
  • @rectummelancolique 我仍然怀疑您的解决方案能否奏效。如果“按钮”是 GUI 组件,它们可能是不可复制的,并且它们可能是多态的。在这种情况下,容器中的实际元素是指针,这意味着不能使用remove_if
  • @James Kanze,我只是重复使用 OP 提供的示例。无论如何,我不明白你为什么不能使用remove_if。如果它们不可复制,那么它们首先不能合理地放在vector 中。如果它们是指针,在上面,在调用擦除之前从end 迭代到vec.end() 以删除对象很简单。更不用说使用智能指针代替常规指针了。
  • @rectummelancolique 我意识到这一点。但是,如果该示例对文本没有意义? (而且这个例子显然没有解释一切。) WRT 指针:有太多我们不知道要回答的问题(正式地,在从容器中删除之前从容器中删除指针是未定义的行为。但它会起作用在实践中,除非其他代码尝试使用它。)
【解决方案2】:

几乎有两种方法可以可靠地做到这一点:

  1. 遍历原始容器的副本并操作原始容器。这可能不可行,除非您的容器存储指针,而不是直接存储实际元素。

  2. 不允许直接操作容器,而是以某种方式标记要删除的元素并在迭代后清除它们。您还可以通过将新元素插入单独的临时容器并在循环完成后附加到原始容器来支持添加新元素 - 您也可以对已删除的元素执行此操作,从而无需在元素本身中存储“已删除”标志.这当然可以用合适的addremove 函数抽象出来。

编辑: 解决方案#2 的删除部分可以使用rectummelancolique 所示的擦除删除习语很好地完成。

【讨论】:

  • 第二种解决方案是规范解决方案,但我不确定它是否适用于此处(或者至少,它需要一些额外的工作)。在他的示例中,处理按钮 4 可能会删除按钮 9,因此他不应在循环的后面处理按钮 9。
  • @JamesKanze:嗯,你可以跳过那些在迭代时被标记的元素。但是,我不确定在他的用例中这样做实际上是一个好主意 - 它会产生不对称性,例如,处理元素 2 会导致删除元素 1 和 3,只有 #3 可以跳过因为 #1 已经处理完毕。
  • @JohannesD 我不知道。事实上,假设按钮是 GUI 界面中的按钮,我一般对迭代按钮的方法表示怀疑。在大多数情况下,您会希望按照事件到达的顺序处理事件:您应该使用经典的 FIFO 队列对事件进行排队。
  • @JohannesD 重新编辑:如果有问题的“按钮”是 GUI 组件,它们可能不可复制并且可能是多态的。在这种情况下,容器中的对象是指针,不能在容器上使用remove_if
【解决方案3】:

由于有按钮(我希望没有太多),您可能希望为每个按钮添加一个标志,以告知它是否已完全处理或类似的东西。然后你在数组中寻找第一个没有被处理过的项目并处理它。您重复此操作,直到处理完所有项目。

for (;;) // breaks, when all items have been processed.
{
    auto it = std::find( std::begin(Items), std::end(Items), 
        [](const Item & item){ return item.hasBeenProcessed(); }
    if ( it == std::end(Items) )
        break;
    process( *it );
}

这应该是安全的。请注意,这可能具有相对于项目数量的二次时间复杂度。正如我所说,希望不会有太多的项目。如果这是一个问题,您可能需要稍微优化此循环,例如从您上次离开的位置开始搜索。但只有当它成为一个问题时才这样做。

【讨论】:

  • 这或多或少也是我的建议(尽管我会写得很干净,没有中断)。如果对象真的是按钮,我认为二次时间不是问题;人类用户在处理它们的时间内可以按下多少个按钮?
【解决方案4】:

既然您在谈论按钮和按钮事件: 最简单的解决方案是简单地将循环重置为开始时 你处理一个事件:

for ( auto current = items.begin(); current != items.end(); ++ current ) {
    if ( current->hasEventWhichNeedsProcessing() ) {
        current->processEvent();    //  possibly invalidates current
        current = items.begin();    //  revalidates current
    }
}

如果我们谈论的是按钮事件(由于 人类用户操作),这应该是安全的,因为你应该 通常能够在新事件之前处理所有事件 发生。 (对于非常迅速发生的事件,您可能永远无法到达 最后的条目。)

不过,我仍然不确定这是否是最佳解决方案。 不管你如何迭代,这意味着你可以对待 事件的顺序与它们到达的顺序不同。更好的解决方案 将事件本身推送到列表中,然后 按顺序处理此列表(作为队列)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-07-01
    • 1970-01-01
    • 2019-11-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多