【问题标题】:Can you remove elements from a std::list while iterating through it?您可以在迭代时从 std::list 中删除元素吗?
【发布时间】:2010-10-10 10:00:34
【问题描述】:

我的代码如下所示:

for (std::list<item*>::iterator i=items.begin();i!=items.end();i++)
{
    bool isActive = (*i)->update();
    //if (!isActive) 
    //  items.remove(*i); 
    //else
       other_code_involving(*i);
}
items.remove_if(CheckItemNotActive);

我想在更新后立即删除不活动的项目,以避免再次遍历列表。但是,如果我添加注释掉的行,当我到达i++ 时会出现错误:“列表迭代器不可递增”。我尝试了一些在 for 语句中没有增加的替代方法,但我什么也做不了。

当您在 std::list 中行走时删除项目的最佳方法是什么?

【问题讨论】:

  • 我还没有看到任何基于向后迭代的解决方案。我发布了one such

标签: c++ list std


【解决方案1】:

您必须首先增加迭代器(使用 i++),然后删除前一个元素(例如,使用 i++ 的返回值)。您可以将代码更改为 while 循环,如下所示:

std::list<item*>::iterator i = items.begin();
while (i != items.end())
{
    bool isActive = (*i)->update();
    if (!isActive)
    {
        items.erase(i++);  // alternatively, i = items.erase(i);
    }
    else
    {
        other_code_involving(*i);
        ++i;
    }
}

【讨论】:

  • 实际上,这并不能保证有效。使用“erase(i++);”,我们只知道将预递增的值传递给erase(),而i在分号之前递增,不一定在调用erase()之前。 “迭代器上一个 = i++;擦除(上一个);”肯定会工作,就像使用返回值一样
  • 不詹姆斯,在调用erase之前i会增加,之前的值会传递给函数。在调用函数之前,必须对函数的参数进行全面评估。
  • @James Curran:这不正确。在调用函数之前,所有参数都经过全面评估。
  • 马丁·约克是正确的。函数调用的所有参数在调用函数之前都会被完全评估,没有例外。这就是函数的工作原理。它与您的 foo.b(i++).c(i++) 示例无关(在任何情况下都未定义)
  • 替代用法i = items.erase(i) 更安全,因为它等效于列表,但如果有人将容器更改为向量,它仍然可以工作。使用向量,erase() 将所有内容向左移动以填充孔。如果您尝试使用在擦除后递增迭代器的代码删除最后一项,则末端向左移动,而迭代器向右移动-过去末端。然后你崩溃了。
【解决方案2】:

你想做的:

i= items.erase(i);

这将正确更新迭代器以指向您删除的迭代器之后的位置。

【讨论】:

  • 请注意,您不能只将该代码放入您的 for 循环中。否则,每次删除一个元素时都会跳过一个元素。
  • 他能不能不做我——;每次都按照他的代码来避免跳过?
  • @therapygeek,如果i==items.begin() 会发生什么?
  • @modalgeek,此时你应该只做i= items.erase(i);。它是规范的形式,已经处理了所有这些细节。
  • 迈克尔指出了一个巨大的“陷阱”,我刚才不得不处理同样的事情。我发现避免它的最简单方法是将 for() 循环分解为 while() 并小心递增
【解决方案3】:

你需要结合 Kristo 的回答和 MSN 的回答:

// Note: Using the pre-increment operator is preferred for iterators because
//       there can be a performance gain.
//
// Note: As long as you are iterating from beginning to end, without inserting
//       along the way you can safely save end once; otherwise get it at the
//       top of each loop.

std::list< item * >::iterator iter = items.begin();
std::list< item * >::iterator end  = items.end();

while (iter != end)
{
    item * pItem = *iter;

    if (pItem->update() == true)
    {
        other_code_involving(pItem);
        ++iter;
    }
    else
    {
        // BTW, who is deleting pItem, a.k.a. (*iter)?
        iter = items.erase(iter);
    }
}

当然,最高效且最适合 SuperCool® STL 的东西应该是这样的:

// This implementation of update executes other_code_involving(Item *) if
// this instance needs updating.
//
// This method returns true if this still needs future updates.
//
bool Item::update(void)
{
    if (m_needsUpdates == true)
    {
        m_needsUpdates = other_code_involving(this);
    }

    return (m_needsUpdates);
}

// This call does everything the previous loop did!!! (Including the fact
// that it isn't deleting the items that are erased!)
items.remove_if(std::not1(std::mem_fun(&Item::update)));

【讨论】:

  • 我确实考虑了您的 SuperCool 方法,但我的犹豫是对 remove_if 的调用并未明确表明目标是处理项目,而不是从活动项目列表中删除它们。 (这些项目不会被删除,因为它们只是变得不活动,而不是不需要)
  • 我想你是对的。一方面我倾向于建议更改 'update' 的名称以消除晦涩难懂,但事实是,这段代码与仿函数很合拍,但它也绝非晦涩难懂。
  • 公平评论,要么修复while循环以使用end,要么删除未使用的定义。
  • 旁注:std::not1std::mem_fun 已被弃用。
【解决方案4】:

使用std::remove_if 算法。

编辑:
使用集合应该是这样的:

  1. 准备收藏。
  2. 进程集合。

如果你不混合这些步骤,生活会更轻松。

  1. std::remove_if。或list::remove_if(如果您知道您使用的是列表而不是TCollection
  2. std::for_each

【讨论】:

  • std::list 有一个 remove_if 成员函数,它比 remove_if 算法更有效(并且不需要“remove-erase”习语)。
  • @brianneal 这取决于您是否可以访问 C++20 功能,否则 C++17 仅提供 std::remove_if en.cppreference.com/w/cpp/algorithm/remove
  • @Antonio 我说的是 std::list 的 remove_if member 函数。我已经离开 C++ 很长时间了,但是当我写我的评论时(超过 11 年前!)那是一件事,我很确定它仍然是。 cplusplus.com/reference/list/list/remove_if
  • @BrianNeal 是的,我不知道我误解了你的评论,实际上很清楚。
【解决方案5】:

我总结了一下,下面是三个方法的例子:

1。使用while循环

list<int> lst{4, 1, 2, 3, 5};

auto it = lst.begin();
while (it != lst.end()){
    if((*it % 2) == 1){
        it = lst.erase(it);// erase and go to next
    } else{
        ++it;  // go to next
    }
}

for(auto it:lst)cout<<it<<" ";
cout<<endl;  //4 2

2。在列表中使用remove_if 成员函数:

list<int> lst{4, 1, 2, 3, 5};

lst.remove_if([](int a){return a % 2 == 1;});

for(auto it:lst)cout<<it<<" ";
cout<<endl;  //4 2

3。使用std::remove_if函数结合erase成员函数:

list<int> lst{4, 1, 2, 3, 5};

lst.erase(std::remove_if(lst.begin(), lst.end(), [](int a){
    return a % 2 == 1;
}), lst.end());

for(auto it:lst)cout<<it<<" ";
cout<<endl;  //4 2

4。使用for循环,要注意更新迭代器:

list<int> lst{4, 1, 2, 3, 5};

for(auto it = lst.begin(); it != lst.end();++it){
    if ((*it % 2) == 1){
        it = lst.erase(it);  erase and go to next(erase will return the next iterator)
        --it;  // as it will be add again in for, so we go back one step
    }
}

for(auto it:lst)cout<<it<<" ";
cout<<endl;  //4 2 

【讨论】:

  • 在 C++20 中,您只能使用 std::erase_if(lst, pred)。它与选项 2 和 3 基本相同,但更短,适用于任何类型的容器。
【解决方案6】:

克里斯托的答案的替代循环版本。

您会失去一些效率,在删除时您会后退然后再次前进,但作为额外迭代器增量的交换,您可以在循环范围内声明迭代器,并且代码看起来更简洁。选择什么取决于当前的优先事项。

答案完全过时了,我知道...

typedef std::list<item*>::iterator item_iterator;

for(item_iterator i = items.begin(); i != items.end(); ++i)
{
    bool isActive = (*i)->update();

    if (!isActive)
    {
        items.erase(i--); 
    }
    else
    {
        other_code_involving(*i);
    }
}

【讨论】:

  • 这也是我用过的。但是我不确定如果要删除的元素是容器中的第一个元素,它是否可以保证工作。对我来说,我认为它可以工作,但我不确定它是否可以跨平台移植。
  • 我没有做“-1”,但是,列表迭代器不能递减吗?至少我得到了 Visual Studio 2008 的断言。
  • 只要链表实现为具有头/存根节点的循环双链表(用作 end() rbegin() 并且当为空时也用作 begin() 和 rend() ) 这将起作用。我不记得我在哪个平台上使用它,但它也对我有用,因为上面命名的实现是 std::list 最常见的实现。但无论如何,几乎可以肯定这是在利用一些未定义(按 C++ 标准)的行为,所以最好不要使用它。
  • re: iterator cannot be decremented erase 方法需要 random access iterator。一些集合实现提供了一个forward only iterator,它会导致断言。
  • @Jesse Chisholm 问题是关于 std::list,而不是任意容器。 std::list 提供擦除和双向迭代器。
【解决方案7】:

这是一个使用 for 循环的示例,该循环迭代列表并在遍历列表期间删除项目时递增或重新验证迭代器。

for(auto i = items.begin(); i != items.end();)
{
    if(bool isActive = (*i)->update())
    {
        other_code_involving(*i);
        ++i;

    }
    else
    {
        i = items.erase(i);

    }

}

items.remove_if(CheckItemNotActive);

【讨论】:

    【解决方案8】:

    移除只会使指向被移除元素的迭代器失效。

    所以在这种情况下,删除 *i 后, i 无效,您无法对其进行增量。

    你可以做的是先保存要删除的元素的迭代器,然后递增迭代器,然后删除保存的那个。

    【讨论】:

    • 使用后增量要优雅得多。
    【解决方案9】:

    如果您将std::list 视为一个队列,那么您可以将所有要保留的项目出队并入队,但只能出队(而不是入队)您要删除的项目。这是一个示例,我想从包含数字 1-10 的列表中删除 5...

    std::list<int> myList;
    
    int size = myList.size(); // The size needs to be saved to iterate through the whole thing
    
    for (int i = 0; i < size; ++i)
    {
        int val = myList.back()
        myList.pop_back() // dequeue
        if (val != 5)
        {
             myList.push_front(val) // enqueue if not 5
        }
    }
    

    myList 现在只有数字 1-4 和 6-10。

    【讨论】:

    • 有趣的方法,但我担心它可能会很慢。
    【解决方案10】:

    向后迭代避免了擦除一个元素对剩余要遍历的元素的影响:

    typedef list<item*> list_t;
    for ( list_t::iterator it = items.end() ; it != items.begin() ; ) {
        --it;
        bool remove = <determine whether to remove>
        if ( remove ) {
            items.erase( it );
        }
    }
    

    PS:参见this,例如,关于反向迭代。

    PS2:我没有彻底测试它是否能很好地处理末尾的擦除元素。

    【讨论】:

    • 回复:avoids the effect of erasing an element on the remaining elements 的列表,可能是的。对于向量可能不是。这不是任意集合的保证。例如,地图可能决定重新平衡自身。
    【解决方案11】:

    你可以写

    std::list<item*>::iterator i = items.begin();
    while (i != items.end())
    {
        bool isActive = (*i)->update();
        if (!isActive) {
            i = items.erase(i); 
        } else {
            other_code_involving(*i);
            i++;
        }
    }
    

    你可以用std::list::remove_if写等价的代码,更简洁更明确

    items.remove_if([] (item*i) {
        bool isActive = (*i)->update();
        if (!isActive) 
            return true;
    
        other_code_involving(*i);
        return false;
    });
    

    std::vector::erasestd::remove_if 习惯用法应该在 items 是向量而不是列表时使用,以保持 O(n) 的复杂性 - 或者如果您编写通用代码并且 items 可能是一个没有有效方法的容器擦除单个项目(如矢量)

    items.erase(std::remove_if(begin(items), end(items), [] (item*i) {
        bool isActive = (*i)->update();
        if (!isActive) 
            return true;
    
        other_code_involving(*i);
        return false;
    }));
    

    【讨论】:

      【解决方案12】:

      做while循环,灵活快速,易读易写。

      auto textRegion = m_pdfTextRegions.begin();
          while(textRegion != m_pdfTextRegions.end())
          {
              if ((*textRegion)->glyphs.empty())
              {
                  m_pdfTextRegions.erase(textRegion);
                  textRegion = m_pdfTextRegions.begin();
              }
              else
                  textRegion++;
          } 
      

      【讨论】:

      • 这是低效的 - 每次删除一个时它都会从头开始重新启动列表。
      【解决方案13】:

      我想分享我的方法。此方法还允许在迭代期间将元素插入到列表的末尾

      #include <iostream>
      #include <list>
      
      int main(int argc, char **argv) {
        std::list<int> d;
        for (int i = 0; i < 12; ++i) {
          d.push_back(i);
        }
      
        auto it = d.begin();
        int nelem = d.size(); // number of current elements
        for (int ielem = 0; ielem < nelem; ++ielem) {
          auto &i = *it;
          if (i % 2 == 0) {
            it = d.erase(it);
          } else {
            if (i % 3 == 0) {
              d.push_back(3*i);
            }
            ++it;
          }
        }
      
        for (auto i : d) {
            std::cout << i << ", ";
        }
        std::cout << std::endl;
        // result should be: 1, 3, 5, 7, 9, 11, 9, 27,
        return 0;
      }
      

      【讨论】:

        【解决方案14】:

        我认为你有一个错误,我是这样编码的:

        for (std::list<CAudioChannel *>::iterator itAudioChannel = audioChannels.begin();
                     itAudioChannel != audioChannels.end(); )
        {
            CAudioChannel *audioChannel = *itAudioChannel;
            std::list<CAudioChannel *>::iterator itCurrentAudioChannel = itAudioChannel;
            itAudioChannel++;
        
            if (audioChannel->destroyMe)
            {
                audioChannels.erase(itCurrentAudioChannel);
                delete audioChannel;
                continue;
            }
            audioChannel->Mix(outBuffer, numSamples);
        }
        

        【讨论】:

        • 我猜这是因为样式偏好而被否决,因为它看起来很实用。是的,当然,(1) 它使用了一个额外的迭代器,(2) 迭代器增量位于循环的奇怪位置,没有充分的理由将其放在那里, (3​​) 它在决定删除之后而不是像 OP 中那样在之前执行通道工作。但这不是一个错误的答案。
        猜你喜欢
        • 2019-03-10
        • 2016-07-15
        • 2010-12-05
        • 2011-07-13
        • 2011-02-21
        • 2014-01-04
        相关资源
        最近更新 更多