从向量中删除项目,而在C ++ 11范围“ for”循环中?


97

我有一个IInventory *的向量,并且正在使用C ++ 11范围遍历该列表,以便对每个对象进行处理。

在处理完一个东西之后,我可能想将其从列表中删除并删除该对象。我知道我可以随时调用delete指针进行清理,但是在范围for循环中将其从向量中删除的正确方法是什么?如果我从列表中删除它,我的循环是否将无效?

std::vector<IInventory*> inv;
inv.push_back(new Foo());
inv.push_back(new Bar());

for (IInventory* index : inv)
{
    // Do some stuff
    // OK, I decided I need to remove this object from 'inv'...
}

8
如果想花哨的话,可以使用std::remove_if“做某事”的谓词,然后如果要删除该元素,则返回true。
本杰明·林德利

为什么不能仅添加索引计数器然后使用inv.erase(index)之类的原因?
TomJ 2012年

4
@TomJ:那仍然会搞砸迭代。
Ben Voigt 2012年

@TomJ会成为性能杀手-每次擦除后,您都需要在擦除后移动所有元素。
伊万

1
@BenVoigt我建议切换到std::list以下位置
bobobobo

Answers:


94

不,你不能。基于范围for是用于需要一次访问容器的每个元素的情况。

for如果您需要在进行过程中修改容器,多次访问元素或以非线性方式遍历容器,则应使用法向循环或其表亲之一。

例如:

auto i = std::begin(inv);

while (i != std::end(inv)) {
    // Do some stuff
    if (blah)
        i = inv.erase(i);
    else
        ++i;
}

5
不适用于这里的删除习语吗?
Naveen

4
@Naveen我决定不尝试这样做,因为显然他需要遍历每个项目,进行计算,然后可能将其从容器中删除。Erase-remove表示,您只是擦除谓词返回的元素trueAFAIU,这样最好不要将迭代逻辑与谓词混用。
塞斯·卡内基

4
@SethCarnegie删除带有lambda的谓词可以优雅地允许(因为这已经是C ++ 11)
Potatoswatter

11
不喜欢这种解决方案,对于大多数容器,它都是O(N ^ 2)。 remove_if更好。
Ben Voigt

6
这个答案正确的,erase返回一个新的有效迭代器。它可能效率不高,但是可以保证工作。
sp2danny

56

每次从向量中删除元素时,您都必须假定已删除元素处或之后的迭代器不再有效,因为已删除元素之后的每个元素都会移动。

基于范围的for循环只是使用迭代器的“常规”循环的语法糖,因此适用于上述情况。

话虽如此,您可以简单地:

inv.erase(
    std::remove_if(
        inv.begin(),
        inv.end(),
        [](IInventory* element) -> bool {
            // Do "some stuff", then return true if element should be removed.
            return true;
        }
    ),
    inv.end()
);

5
“,因为vector可能重新分配了保留其元素的内存块。 ”不,a vector永远不会由于对的调用而重新分配erase。迭代器无效的原因是因为删除元素之后的每个元素都已移动。
ildjarn 2012年

2
默认的Capture默认值[&]是合适的,以允许他使用局部变量来“做一些事情”。
Potatoswatter 2012年

2
这看起来并不比基于迭代器的循环简单,此外,您还必须记住将其括remove_if起来.erase,否则什么也不会发生。
bobobobo

2
@bobobobo如果使用“基于迭代器的循环”,则表示Seth Carnegie的答案,即平均O(n ^ 2)。std::remove_if是O(n)。
Branko Dimitrijevic

通过将元素交换到列表的后面并避免实际移动元素,直到完成所有“待删除”的操作(remove_if必须在内部进行操作),您才是真正的好主意。但是,如果您有vector5个元素,而.erase() 一次只有1 个元素,则使用迭代器vs不会对性能造成影响remove_if。如果列表较大,那么您实际上应该切换到std::list列表中间删除很多的地方。
bobobobo

16

理想情况下,在迭代向量时,最好不要修改向量。使用擦除删除惯用语。如果这样做,您可能会遇到一些问题。由于在vector一个erase会使所有迭代器与元素开始被擦除高达的end(),你需要确保你的迭代器仍然有效使用:

for (MyVector::iterator b = v.begin(); b != v.end();) { 
    if (foo) {
       b = v.erase( b ); // reseat iterator to a valid value post-erase
    else {
       ++b;
    }
}

注意,您需要b != v.end()原样进行测试。如果尝试按以下方式对其进行优化:

for (MyVector::iterator b = v.begin(), e = v.end(); b != e;)

您将在UB中遇到问题,因为您eerase电话在第一次通话后就无效了。


@ildjarn:是的,是的!那是一个错字。
2012年

1
这不是擦除删除惯用语。没有调用std::remove,它是O(N ^ 2)而不是O(N)。
Potatoswatter 2012年

1
@Potatoswatter:当然不是。我试图指出在迭代时删除的问题。我想我的措辞没有通过?
2012年

5

是否严格要求在该循环中删除元素?否则,您可以将要删除的指针设置为NULL,然后再次遍历向量以删除所有NULL指针。

std::vector<IInventory*> inv;
inv.push_back( new Foo() );
inv.push_back( new Bar() );

for ( IInventory* &index : inv )
{
    // do some stuff
    // ok I decided I need to remove this object from inv...?
    if (do_delete_index)
    {
        delete index;
        index = NULL;
    }
}
std::remove(inv.begin(), inv.end(), NULL);

1

抱歉,我无法进行死灵尸体发布,也很抱歉,如果我的c ++专业知识妨碍了我的回答,但是如果您尝试遍历每一项并进行可能的更改(例如擦除索引),请尝试使用backword for循环。

for(int x=vector.getsize(); x>0; x--){

//do stuff
//erase index x

}

当删除索引x时,下一个循环将用于最后一次迭代的“前面”项。我真的希望这对某人有所帮助


只是不要忘记使用x来访问某个索引时,做x-1大声笑
lilbigwill99

1

好的,我迟到了,但是无论如何:对不起,不正确,到目前为止,我读到的内容可能的,您只需要两个迭代器即可:

std::vector<IInventory*>::iterator current = inv.begin();
for (IInventory* index : inv)
{
    if(/* ... */)
    {
        delete index;
    }
    else
    {
        *current++ = index;
    }
}
inv.erase(current, inv.end());

仅修改迭代器指向的值不会使任何其他迭代器无效,因此我们可以不必担心。其实,std::remove_if(至少是gcc实现)执行了非常相似的操作(使用经典循环...),只是不删除任何内容并且不会擦除。

但是请注意,这不是线程安全的(!)-但是,这也适用于上述其他一些解决方案...


有没有搞错。这太过分了。
凯西

@Kesse真的吗?这是向量可获得的最有效的算法(无论是基于范围的循环还是经典的迭代器循环都没有关系):这样,您最多可以将向量中的每个元素移动一次,然后对整个向量进行一次精确的迭代。如果您删除每个匹配的元素,您将移动随后的元素多少次并遍历向量erase(当然,前提是要删除多个元素)?
阿空加瓜

1

我将以示例显示,下面的示例从向量中删除奇数元素:

void test_del_vector(){
    std::vector<int> vecInt{0, 1, 2, 3, 4, 5};

    //method 1
    for(auto it = vecInt.begin();it != vecInt.end();){
        if(*it % 2){// remove all the odds
            it = vecInt.erase(it);
        } else{
            ++it;
        }
    }

    // output all the remaining elements
    for(auto const& it:vecInt)std::cout<<it;
    std::cout<<std::endl;

    // recreate vecInt, and use method 2
    vecInt = {0, 1, 2, 3, 4, 5};
    //method 2
    for(auto it=std::begin(vecInt);it!=std::end(vecInt);){
        if (*it % 2){
            it = vecInt.erase(it);
        }else{
            ++it;
        }
    }

    // output all the remaining elements
    for(auto const& it:vecInt)std::cout<<it;
    std::cout<<std::endl;

    // recreate vecInt, and use method 3
    vecInt = {0, 1, 2, 3, 4, 5};
    //method 3
    vecInt.erase(std::remove_if(vecInt.begin(), vecInt.end(),
                 [](const int a){return a % 2;}),
                 vecInt.end());

    // output all the remaining elements
    for(auto const& it:vecInt)std::cout<<it;
    std::cout<<std::endl;

}

在下面输出aw:

024
024
024

请记住,方法 erase将返回传递的迭代器的下一个迭代器。

这里开始,我们可以使用更多的generate方法:

template<class Container, class F>
void erase_where(Container& c, F&& f)
{
    c.erase(std::remove_if(c.begin(), c.end(),std::forward<F>(f)),
            c.end());
}

void test_del_vector(){
    std::vector<int> vecInt{0, 1, 2, 3, 4, 5};
    //method 4
    auto is_odd = [](int x){return x % 2;};
    erase_where(vecInt, is_odd);

    // output all the remaining elements
    for(auto const& it:vecInt)std::cout<<it;
    std::cout<<std::endl;    
}

看到这里看看如何使用std::remove_ifhttps://en.cppreference.com/w/cpp/algorithm/remove


1

与该线程标题相反,我将使用两次通过:

#include <algorithm>
#include <vector>

std::vector<IInventory*> inv;
inv.push_back(new Foo());
inv.push_back(new Bar());

std::vector<IInventory*> toDelete;

for (IInventory* index : inv)
{
    // Do some stuff
    if (deleteConditionTrue)
    {
        toDelete.push_back(index);
    }
}

for (IInventory* index : toDelete)
{
    inv.erase(std::remove(inv.begin(), inv.end(), index), inv.end());
}

0

一个更优雅的解决方案是切换到std::list(假设您不需要快速随机访问)。

list<Widget*> widgets ; // create and use this..

然后,您可以.remove_if在一行中使用和删除C ++函子:

widgets.remove_if( []( Widget*w ){ return w->isExpired() ; } ) ;

因此,这里我只是写一个接受一个参数(Widget*)的仿函数。返回值是Widget*从列表中删除a的条件。

我觉得这种语法很可口。我不认为我会永远使用remove_if标准::向量 -有这么多inv.begin()inv.end()噪音有你可能最好使用整数索引基于删除或只是一个普通的老规则基于迭代器删除(如图所示下面)。但是std::vector无论如何,您实际上都不应该真正从中间位置删除,因此建议list针对这种频繁删除列表中间位置的情况切换到a 。

不过请注意,我没有得到一个机会,呼吁deleteWidget*被拆除那是。为此,它看起来像这样:

widgets.remove_if( []( Widget*w ){
  bool exp = w->isExpired() ;
  if( exp )  delete w ;       // delete the widget if it was expired
  return exp ;                // remove from widgets list if it was expired
} ) ;

您还可以使用基于常规迭代器的循环,如下所示:

//                                                              NO INCREMENT v
for( list<Widget*>::iterator iter = widgets.begin() ; iter != widgets.end() ; )
{
  if( (*iter)->isExpired() )
  {
    delete( *iter ) ;
    iter = widgets.erase( iter ) ; // _advances_ iter, so this loop is not infinite
  }
  else
    ++iter ;
}

如果您不喜欢的长度for( list<Widget*>::iterator iter = widgets.begin() ; ...,可以使用

for( auto iter = widgets.begin() ; ...

1
我不认为你了解如何remove_ifstd::vector实际工作中,以及它是如何保持的复杂性O(N)。
Ben Voigt

没关系 从a的中间删除std::vector总是会在删除每个元素之后滑动每个元素,这是std::list一个更好的选择。
bobobobo

1
不会,它不会“将每个元素向上滑动”。 remove_if将每个元素向上滑动释放的空间数。在您考虑缓存使用情况时,remove_ifstd::vector性能可能优于从中删除缓存的效果std::list。并保留O(1)随机访问。
Ben Voigt 2013年

1
然后,您在寻找问题时会有很好的答案。 这个问题讨论的是迭代列表,两个容器的列表均为O(N)。并删除O(N)元素,这两个容器也都是O(N)。
Ben Voigt

2
不需要预标记;一次完成完全有可能。您只需要跟踪“要检查的下一个元素”和“要填充的下一个插槽”。可以将其视为构建列表的副本,并根据谓词进行过滤。如果谓词返回true,则跳过该元素,否则将其复制。但是列表副本已就位,并且使用交换/移动来代替复制。
Ben Voigt

0

我想我会做以下...

for (auto itr = inv.begin(); itr != inv.end();)
{
   // Do some stuff
   if (OK, I decided I need to remove this object from 'inv')
      itr = inv.erase(itr);
   else
      ++itr;
}

0

您无法在循环迭代期间删除迭代器,因为迭代器计数不匹配,并且在进行某些迭代后您将拥有无效的迭代器。

解决方案:1)获取原始向量的副本2)使用该副本迭代迭代器2)做一些事情并将其从原始向量中删除。

std::vector<IInventory*> inv;
inv.push_back(new Foo());
inv.push_back(new Bar());

std::vector<IInventory*> copyinv = inv;
iteratorCout = 0;
for (IInventory* index : copyinv)
{
    // Do some stuff
    // OK, I decided I need to remove this object from 'inv'...
    inv.erase(inv.begin() + iteratorCout);
    iteratorCout++;
}  

0

一对一擦除元素很容易导致N ^ 2性能。最好标记要擦除的元素,并在循环后立即擦除它们。如果我可以在您向量中的无效元素中假定nullptr,那么

std::vector<IInventory*> inv;
// ... push some elements to inv
for (IInventory*& index : inv)
{
    // Do some stuff
    // OK, I decided I need to remove this object from 'inv'...
    {
      delete index;
      index =nullptr;
    }
}
inv.erase( std::remove( begin( inv ), end( inv ), nullptr ), end( inv ) ); 

应该管用。

如果您的“做一些事情”没有改变vector的元素,而只是用于决定删除或保留该元素,则可以将其转换为lambda(如某人先前的帖子中所建议)并使用

inv.erase( std::remove_if( begin( inv ), end( inv ), []( Inventory* i )
  {
    // DO some stuff
    return OK, I decided I need to remove this object from 'inv'...
  } ), end( inv ) );
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.