在擦除()之后保留一个有效的向量::迭代器

Keeping a valid vector::iterator after erase()

本文关键字:有效 向量 迭代器 一个 擦除 之后 保留      更新时间:2023-10-16

EDIT:我得到了很多答案,告诉我应该将删除分离到另一个循环中。也许我说得不够清楚,但我在最后一段中说,我想找到一个除此之外的解决方案。即保持当前的代码结构,但使用一些鲜为人知的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();
}

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

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

你不能真正在同一时间迭代和突变std::向量,除非迭代之间有一些通信,即突变。

我见过其他非标准容器通过"智能"迭代器来实现这一点,这些迭代器知道它们的值何时被擦除(可能会自动跳到下一项)。不过,这更像是在记账。

我建议您重组代码,不要将更新(删除某些元素)和聚合(相加值)这两种不同的操作混合在一起。

您可以通过将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,但我不确定将其与修改序列中对象的谓词一起使用是否可能/有效。

如果您可以在更新功能中同时传达擦除意图和id,那么您可以这样做:

std::tuple<int, bool> Child::update() {
   auto erase = x();
   return {y, erase};
}
void Parent::update() {
   int i = 0;
   for(vector<A>::iterator it = child.begin(); it != child.end();) {
      auto [y, erase] += (*it)->update();
      i += y;
      if (erase) {
         it = child.erase(it); // erase returns next iterator
      } else {
         ++it;
      }
   }
}

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

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

我不确定你的所有设计约束/目标,但你可以通过它的公共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

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

但是,向量往往比列表具有更好的性能,这是因为引用的局部性要好得多,而且还会增加大量内存开销(在每个节点的前一个和下一个指针中,但也以系统分配器中每个分配块的开销的形式)。

然后,我所做的是,根据一些类似的要求,坚持使用向量,但在删除元素时,允许向量中出现空洞或"死条目"。

在有序迭代过程中,您需要能够检测和跳过向量中的死条目,但您可以折叠向量以定期删除这些死条目(或者在删除元素的特定迭代循环完成后)。您还可以(如有必要)包括一个免费列表,用于为新的子项重用死条目。

我在下面的博客文章中更详细地描述了这种设置:http://upcoder.com/12/vector-hosted-lists/

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

尝试以下修改版本:

   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();

我认为问题是,您的方法"update"也会以与std::vector::erase完全相同的方式使迭代器无效。因此,我建议您只做完全相同的事情:返回新的、有效的迭代器,并相应地对调用循环进行apdapt。