问题 在erase()之后保持有效的vector :: iterator


编辑:我有很多答案告诉我,我应该将删除分成另一个循环。也许我没有说清楚,但我在上一段中说过,我想找到一个解决方案,而不是那个。即保持当前的代码结构,但使用一些鲜为人知的C ++ fu来使其工作。

好吧,我知道那个叫 erase() 在向量上使元素的迭代器和它之后的所有迭代器失效 erase() 将迭代器返回到下一个有效的迭代器,但是如果擦除发生在其他地方怎么办?

我有以下情况(简化):

警告:不要认为这是整个代码。下面显示的内容非常简单,以说明我的问题。下面显示的所有类和方法实际上要复杂得多。

class Child {
   Parent *parent;
}

class Parent {
   vector<Child*> child;
}

void Parent::erase(Child* a) {
   // find an iterator, it, that points to Child* a
   child.erase(it);
}

int Child::update() {
   if(x()) parent.erase(*this) // Sometimes it will; sometimes (most) it won't
   return y;
}

void Parent::update() {
   int i = 0;
   for(vector<A>::iterator it = child.begin(); it != child.end(); it++)
      i += (*it)->update();
}

所以,很明显,它会在运行后崩溃 (*it)->update() 如果 x() 返回true,因为当它执行时,Child将告诉Parent将其从向量中移除,使迭代器无效。

除了制作之外,还有什么方法可以解决这个问题 Parent::erase() 将迭代器一直传递回去 Parent::update()?这将是有问题的,因为每次调用都没有调用它 Child::update()因此,该函数需要一种方法来每隔一次将迭代器返回给自己,并且它当前还返回另一个值。我还希望避免使用其他类似方法将擦除过程与更新循环分开。


4485
2018-05-23 10:58


起源

你能把容器改成吗? std::list?如果是这样,我删除的答案中的迭代增量操作将起作用...当然可能还有其他副作用,这些副作用在您发布的代码中并不明显... - Nim
你确定你应该使用 std::vector?它有快速的随机访问,但插入和删除速度很慢,所以如果你要做的很多,并且通常会迭代所有内容, std::list 会更好。 - Jan Hudec
@Nim和@Jan Hudec:实际上,擦除很少发生(相对),其中最常见的运算符是遍历批次的迭代,随机访问,这就是我选择向量的原因;除非有更好的选择?编辑:哦,添加新元素并不重要;矢量说的是“这就是我拥有的所有东西”。 - Infiltrator
FWIW,Qt在其对象上有一个“deleteLater()”,表明解决此问题的常用方法是将删除推迟到更新之外。 - Macke
STL迭代器失效规则: stackoverflow.com/questions/6438086/iterator-invalidation-rules - ideawu


答案:


你不能真正迭代并同时改变一个std :: vector,除非在迭代之间有一些通信,即变异。

我已经看到其他非标准容器通过“智能”迭代器来实现,这些迭代器知道它们的值何时被删除(并且可能自动跳转到下一个项目)。虽然它更加保持书籍。


3
2018-05-23 12:13



现在这就是我喜欢听到的。这些容器是什么? - Infiltrator
@Tim:这些在STL中不可用(忘了强调,现在已经修复)。我已经看过它们用于Java,你当然可以用C ++编写它们作为一种有趣的练习。 OTOH,创建/递增/销毁迭代器会有更多的开销,所以要走两次列表 将会 更好的选择。 - Macke
这不是我喜欢听到的。 :P嗯,我希望有一个“酷”的选择,但我想我必须使用deleteme或second loop方法。 - Infiltrator
@Tim:是的对于那个很抱歉。 :-p我猜这些“跟踪”迭代器只是膨胀以符合C ++社区的味道。另外,从迭代器下面拉出地板 是 一个棘手的操作,因为它失效太多了。请注意,使用大量的shared_ptr可能会有所帮助,但那些标准已经不太长了。 - Macke
是的,我对shared_ptr并不太熟悉,但我认为在这种情况下切换到那些可能会更好。然后,在不太了解它们的情况下,我最终会错误地使用它们。另一方面,除非我这样做,否则我不会学习如何使用它们。 :P - Infiltrator


我建议您重新构建代码,以便不混合两种不同的更新操作(通过删除某些元素)和聚合(通过累加值)数据。

你可以通过改变的返回值来做到这一点 Child::update 喜欢的东西 std::pair<int, bool>,哪里 int 是价值和 bool 指示是否应删除此元素。

如果你能做到 Child::update 一个 const 方法(意思是它不修改对象,只调用其他方法 const 方法),您可以编写一个可以使用的简单仿函数 std::remove_if。像这样的东西:

class update_delete {
public:
    update_delete() : sum(0) {}
    bool operator()(const Child & child) {
        std::pair<int, bool> result = child.update();
        sum += result.first;
        return result.second;
    }
private:
    int sum;
}

如果你做不到 update 它 const,只需将元素与后面的某个元素交换(您必须保留一个迭代器,该迭代器始终指向可用于交换的最后一个元素)。完成聚合后,只需丢弃向量的末尾(现在包含要删除的所有元素) vector::resize。这类似于使用 std::remove_if,但我不确定是否可以将它与修改序列中对象的谓词一起使用。


4
2018-05-23 11:01



我通过分离聚合和删除来看出你的意思。但是,如果有办法按照我想要的方式行事,我宁愿不这样做。但是,关于答案的第二部分,更新方法的定义不能是const;毕竟,它需要更新一些东西。 :P - Infiltrator
@Tim:我怀疑同样多,但你的例子并不清楚。 - Björn Pollex
我很抱歉;我认为命名为“更新”就足够了。 - Infiltrator


如果你可以通过更新函数传递erase-intent和id,那么你可以这样做:

int Child::update(bool& erase) {
   erase = x();
   return y;
}

void Parent::update() {
   int i = 0;

   for(vector<A>::iterator it = child.begin(); it != child.end();) {
      bool erase = false;
      i += (*it)->update(erase);

      if (erase) {
         it = child.erase(it); // erase returns next iterator
      } else {
         ++it;
      }
   }
}

3
2018-05-23 12:06



正如我在我的问题中所说,我宁愿避免这样做,并且询问是否还有其他方法可以做到这一点。不管怎么说,还是要谢谢你。 - Infiltrator
@Tim:我以为你想避免发送迭代器,或者两次走列表。 - Macke
如果我的问题具有误导性,我道歉。问题是Parent :: erase()实际上需要做的不仅仅是从向量中删除元素。我想我可以将这本书的其余部分保存在一个单独的函数中,并在调用child.erase()之前调用它。我会牢记这一点;谢谢。但是,我喜欢此时其他解决方案的声音。 - Infiltrator


如何将要删除的子项添加到列表中,并在更新每个子项后删除它们。

实际上,您将推迟删除直到第一个循环之后。


2
2018-05-23 11:17



这只会将同样的问题转移到第二遍。擦除列表中的第一个元素时,列表中的所有剩余迭代器都将变为无效。 - Jan Hudec
与Space_C0wb0y所说的相同;不过还是很好的答案。但是,如果有办法按照我想要的方式行事,我宁愿不这样做。 - Infiltrator
正确的算法:添加要删除的子项 为了,擦除 以相反的顺序。 (或对它们进行排序)。通过以相反的顺序擦除,您不会使尚未擦除的迭代器无效。 - MSalters
@Jan Hudec:不......你可以有第二个清单,然后迭代它;或者像MSalters建议的那样做。 - Skurmedel
@Jan Hudec,@ MSalters和@Skurmedel:是的,这就是我理解它的方式;构建一个单独的列表,也许是另一个包含相同指针的向量,然后在每个指针上调用Parent :: erase()。这样,实际上没有元素从第二个列表中删除,并且它的迭代器将保持有效。然后可以安全地处理第二个列表,因为它只是持有指针。话虽如此,如果可能的话,我宁愿避免使用这种方法。 - Infiltrator


我不确定你所有的设计约束/目标,但是你可以通过它的公共API来揭示删除孩子的需要 Parent::update 有条件地删除。

void Parent::update()
{
    int i = 0;
    for( vector<A>::iterator it = child.begin();
         it != child.end(); /* ++it done conditionally below */ )
    {
        i += (*it)->update();
        if( (*it)->should_erase() )
        {
            it = child.erase( it );
        }
        else
        {
            ++it;
        }
    }
}

bool Child::should_erase() const
{
    return x();
}

int Child::update()
{
    // Possibly do other work here.
    return y;
}

那么也许你可以删除 Parent::erase


1
2018-05-23 12:09



与Macke所说的相似,是的。但是,问题是Parent :: erase()实际上需要做的不仅仅是从向量中删除元素。我想我可以将这本书的其余部分保存在一个单独的函数中,并在调用child.erase()之前调用它。我会牢记这一点;谢谢。 - Infiltrator


解决方案的一个基础(正如其他人所说)是从std :: vector切换到std :: list,因为您可以从列表中擦除节点而不会使对其他节点的引用无效。

但是由于更好,矢量往往比列表具有更好的性能 参考地点 并且还增加了大量的内存开销(在每个节点的prev和next指针中,但也以系统分配器中每个已分配块的开销的形式)。

然后,我做了一些类似的要求,就是坚持使用矢量,但是当元素被删除时,允许在矢量中使用空洞或“死项”。

在顺序迭代期间,您需要能够检测并跳过向量中的死项,但是您可以折叠向量以定期删除这些死项(或者在删除元素已完成的特定迭代循环之后)。您还可以(如有必要)包含一个空闲列表,以便为新孩子重复使用死亡条目。

我在以下博文中更详细地描述了这个设置:: http://upcoder.com/12/vector-hosted-lists/

我在博客文章中谈到的其他一些可能与这些要求相关的设置是“矢量托管”链表和带有自定义内存池的链表。


1
2018-03-02 10:07





尝试下面的修改版本:

   for(vector<A>::iterator it = child.begin(); it != child.end();)
      i += (*it++)->update();

编辑:这当然是可怕的打破,但它 会工作 如果你可以改变容器 std::list。如果没有这种改变,你可以尝试反向迭代器(如果顺序无关紧要),即

   for(vector<A>::reverse_iterator it = child.rbegin(); it != child.rend();)
      i += (*it++)->update();

0
2018-05-23 11:02



我认为重点是父迭代器有时会失效 child->update 运行。移动增量运算符不会改变它。 - sje397
@sje是的;那是对的。迭代器有时会因调用(* it) - > update()而失效;不是大部分时间,但它仍然会发生。如果不清楚我会编辑我的问题吗? - Infiltrator
@Tim:我觉得你的问题非常好。 - sje397
@ sje397,这是我的简单例子: ideone.com/zYLca,我认为它正在模仿OP在做什么,因为我理解他的代码...... - Nim
@ sje397:添加了崩溃发生原因的解释,因此人们不会被抛弃。 - Infiltrator


我认为问题是,你的方法“更新”也会使std :: vector :: erase的方式与迭代器无效。所以我建议你做完全相同的事情:返回新的,有效的迭代器并相应地apdapt调用循环。


0
2018-05-23 11:57



请参阅我的问题的最后两段。 - Infiltrator
@Tim对不起我错过了。我认为问题在于“Child”依赖于“Parent”。方法“Child :: update”有点奇怪的语义,因为它实际上更新了父而不是Child。你可以用类似“bool shouldBeErased()”的东西替换它,并处理从实际拥有该成员的类中的向量中删除“父”。 - b.buchhold
抱歉;这个例子非常简单,以说明我的问题,而Child :: update实际上做了一个 批量 对孩子。我会编辑我的问题以使其更清晰。正如我在上一段中所说,我想知道除此之外是否还有其他办法。我的问题真的令人困惑吗? :\ - Infiltrator
@Tim:仍然,updated()可以进行更新并发出信号,如果它需要被删除到partent。对于exmaple via返回值,设置成员等等。我仍然认为从矢量中删除不应该是“孩子”的责任 - b.buchhold
可以把它当作宣称独立于父母的孩子,因此不再被父母“拥有”。至少,从设计的角度来看,这就是它的作用。但从它的外观来看,我很难编码并且必须重写。 - Infiltrator