在擦除()之后保留一个有效的向量::迭代器
Keeping a valid vector::iterator after erase()
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。
- 在C++中初始化向量映射的最有效方法
- 当映射包含字符串向量作为值时,从值中获取键的有效方法
- C++有效地找到向量中第一个最接近的匹配值?
- 如何有效地操作满足给定谓词的向量中的所有项目?
- 检查两个向量是否并行的最有效方法
- 当表示为对象的一维向量时,有效地旋转 NxM 矩阵 (C++)
- 如何有效地实现将向量的数据分配给多个变量?
- C++去除前x个元素的有效方法,在不改变向量大小的情况下将第x+1个元素推到第一个
- 如何在C++中有效地将数组移动到向量
- 将一种数据类型的向量复制到同一数据类型的结构向量中的有效方法是什么
- 如何有效地用第二个值对向量对进行分组
- 从长(且合理)稀疏向量中选择随机元素的最有效方法是什么?
- 如何在 CUDA 中(有效地)将大量向量相互比较
- 基于整数向量执行位排列的有效方法?
- 如何使用C++有效地合并排序与向量
- C++:检查向量中的元素是否大于另一个具有相同索引的元素的有效方法?
- 如何有效地规范化向量C++
- 为什么优先级队列是使用堆实现的,而我们可以更有效地使用向量来实现它
- 字符串向量的有效组合
- C++中的有效向量运算符/对临时对象的引用