【问题标题】:Removing item from vector, while in C++11 range 'for' loop?从向量中删除项目,而在 C++11 范围“for”循环中?
【发布时间】:2012-05-08 18:45:59
【问题描述】:

我有一个 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'...
}

【问题讨论】:

  • 如果你想变得花哨,你可以使用 std::remove_if 和一个谓词“做事”,然后如果你想删除元素,则返回 true。
  • 有什么理由不能只添加一个索引计数器然后使用类似 inv.erase(index) 的东西吗?
  • @TomJ:那仍然会搞砸迭代。
  • @BenVoigt i-- 删除后。或者使用整数索引向后迭代。
  • @BenVoigt 我建议切换到下面的std::list

标签: c++ vector for-loop c++11


【解决方案1】:

逐个擦除元素很容易导致 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 ) ); 

应该可以。

如果您的“做一些事情”没有更改向量的元素,而仅用于决定删除或保留元素,您可以将其转换为 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 ) );

【讨论】:

    【解决方案2】:

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

    解决方案: 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++;
    }  
    

    【讨论】:

      【解决方案3】:

      与此主题标题相反,我将使用两次传递:

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

      【讨论】:

        【解决方案4】:

        我将举例说明,下面的例子从向量中删除奇数元素:

        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 方法将返回传递的迭代器的下一个迭代器。

        here ,我们可以使用更多的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

        【讨论】:

          【解决方案5】:

          好的,我迟到了,但无论如何:抱歉,我目前阅读的内容不正确 - 可能的,您只需要两个迭代器:

          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 删除每个匹配元素,您将移动后续元素并迭代向量多少次(当然,前提是您删除了多个单个元素)?
          【解决方案6】:

          我想我会做以下事情......

          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;
          }
          

          【讨论】:

            【解决方案7】:

            对不起,如果我的 c++ 专业知识妨碍了我的回答,也很抱歉,但如果您尝试遍历每个项目并进行可能的更改(例如擦除索引),请尝试使用 backwords for 循环。

            for(int x=vector.getsize(); x>0; x--){
            
            //do stuff
            //erase index x
            
            }
            

            当擦除索引 x 时,下一个循环将针对上一次迭代“前面”的项目。我真的希望这对某人有所帮助

            【讨论】:

            • 只是不要忘记使用 x 访问某个索引时,做 x-1 大声笑
            【解决方案8】:

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

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

            例如:

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

            【讨论】:

            • Wouldn't erase-remove idiom 适用于此?
            • @Naveen 我决定不尝试这样做,因为显然他需要遍历每个项目,对其进行计算,然后可能将其从容器中删除。 Erase-remove 表示您只是擦除谓词为其返回 true、AFAIU 的元素,这样似乎最好不要将迭代逻辑与谓词混合。
            • @SethCarnegie Erase-remove 使用 lambda 作为谓词优雅地允许这样做(因为这已经是 C++11)
            • 不喜欢这个解决方案,对于大多数容器来说它是 O(N^2)。 remove_if 更好。
            • 这个答案正确的,erase 返回一个新的有效迭代器。它可能效率不高,但可以保证工作。
            【解决方案9】:

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

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

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

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

            所以在这里我只是在编写一个接受一个参数的函子(Widget*)。返回值是从列表中删除Widget* 的条件。

            我觉得这个语法很好听。我认为我永远不会将remove_if 用于std::vectors - 那里有太多inv.begin()inv.end() 噪音,你可能最好使用an integer-index-based delete 或只是一个普通的旧的基于常规迭代器的删除(如下图)。但无论如何,您不应该真的从 std::vector 的中间删除太多,因此建议在这种频繁删除列表中间的情况下切换到 list

            但请注意,我没有机会在已删除的 Widget* 上致电 delete。为此,它看起来像这样:

            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&lt;Widget*&gt;::iterator iter = widgets.begin() ; ...的长度,可以使用

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

            【讨论】:

            • 我认为你不明白 remove_ifstd::vector 上的实际工作原理,以及它如何将复杂度保持在 O(N)。
            • 没关系。从std::vector 的中间移除总是会在您移除的元素之后滑动每个元素,这使得std::list 成为更好的选择。
            • 不,它不会“将每个元素向上滑动一个”。 remove_if 将每个元素向上滑动释放的空间数。当您考虑缓存使用情况时,std::vector 上的remove_if 可能优于从std::list 中删除。并保留O(1) 随机访问。
            • 那么你有一个很好的答案来寻找一个问题。 This question 谈到迭代列表,这对于两个容器来说都是 O(N)。并移除 O(N) 个元素,这对于两个容器来说也是 O(N) 个。
            • 不需要预先标记;完全有可能一次性完成。您只需要跟踪“要检查的下一个元素”和“要填充的下一个插槽”。将其视为构建列表的副本,根据谓词进行过滤。如果谓词返回 true,则跳过该元素,否则复制它。但是列表副本是就地进行的,使用交换/移动而不是复制。
            【解决方案10】:

            在该循环中删除元素是否有严格要求?否则,您可以将要删除的指针设置为 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);
            

            【讨论】:

              【解决方案11】:

              每次从向量中移除一个元素时,您必须假设被擦除元素处或之后的迭代器不再有效,因为被擦除元素之后的每个元素都被移动了。

              基于范围的 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()
              );
              

              【讨论】:

              • "因为 vector 有可能重新分配了保存其元素的内存块" 不,vector 永远不会因为调用 @ 而重新分配987654324@。迭代器失效的原因是被擦除元素之后的每个元素都被移动了。
              • [&amp;] 的默认捕获是合适的,以允许他使用局部变量“做一些事情”。
              • 这看起来并不比基于迭代器的循环简单,此外你必须记住用.erase 包围你的remove_if,否则什么都不会发生。
              • @bobobobo 如果“基于迭代器的循环”是指Seth Carnegie's answer,则平均为 O(n^2)。 std::remove_if 是 O(n)。
              • @bobobobo 除非你真的需要随机访问。
              【解决方案12】:

              理想情况下,您不应该在迭代向量时修改它。使用擦除删除成语。如果这样做,您可能会遇到一些问题。由于在 vectorerase 中,所有迭代器都无效,从被擦除的元素开始直到 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,因为您的 e 在第一次调用 erase 后无效。

              【讨论】:

              • @ildjarn:是的,没错!那是一个错字。
              • 这不是擦除删除成语。没有调用std::remove,它是 O(N^2) 而不是 O(N)。
              • @Potatoswatter:当然不是。我试图指出迭代时删除的问题。我猜我的措辞没有通过?
              猜你喜欢
              • 2013-12-17
              • 2021-04-08
              • 2019-05-29
              • 2015-02-02
              • 2020-06-24
              相关资源
              最近更新 更多