您可以在遍历 std::list 时从 std::list 中删除元素吗?

Can you remove elements from a std::list while iterating through it?

提问人:AShelly 提问时间:2/28/2009 更新时间:4/25/2023 访问量:296398

问:

我的代码如下所示:

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

我想在更新后立即删除非活动项目,以避免再次浏览列表。但是,如果我添加注释掉的行,当我到达时会收到错误:“列表迭代器不可增量”。我尝试了一些在 for 语句中没有递增的替代方法,但我无法让任何东西起作用。i++

在浏览 std::list 时删除项目的最佳方法是什么?

C++ 列表 std

评论

0赞 sancho.s ReinstateMonicaCellio 3/13/2019
我还没有看到任何基于向后迭代的解决方案。我发布了一个这样的

答:

336赞 Michael Kristofik 2/28/2009 #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;
    }
}

评论

9赞 James Curran 2/28/2009
实际上,这并不能保证有效。使用 “erase(i++);”,我们只知道预先递增的值被传递给 erase(),并且 i 在分号之前递增,而不一定在调用 erase() 之前。“迭代器上一个 = i++;erase(prev);“肯定会起作用,就像使用返回值一样
75赞 Brian Neal 2/28/2009
没有 James,i 在调用 erase 之前递增,并且将前一个值传递给函数。在调用函数之前,必须对函数的参数进行全面评估。
37赞 Martin York 2/28/2009
@James Curran:这是不正确的。在调用函数之前,将完全计算所有参数。
9赞 jalf 2/28/2009
马丁·约克是对的。在调用函数之前,函数调用的所有参数都会被完全计算,没有例外。这就是函数的工作方式。它与你的 foo.b(i++).c(i++) 示例无关(在任何情况下都是未定义的)
93赞 Eric Seppanen 5/3/2012
替代用法更安全,因为它等同于列表,但如果有人将容器更改为向量,它仍然有效。使用向量,erase() 将所有内容向左移动以填充孔。如果尝试使用在擦除后递增迭代器的代码删除最后一项,则末端向左移动,迭代器向右移动 - 超过末尾。然后你崩溃了。i = items.erase(i)
157赞 MSN 2/28/2009 #2

您要执行以下操作:

i= items.erase(i);

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

评论

95赞 Michael Kristofik 2/28/2009
请注意,您不能只是将该代码放入 for 循环中。否则,每次删除元素时都会跳过一个元素。
2赞 enthusiasticgeek 3/19/2012
他能不做我吗?每次都跟着他的一段代码避免跳过?
1赞 MSN 3/20/2012
@enthusiasticgeek,如果会发生什么?i==items.begin()
1赞 MSN 3/20/2012
@enthusiasticgeek,在这一点上,你应该做.它是规范形式,并且已经处理了所有这些细节。i= items.erase(i);
8赞 Anne Quinn 1/14/2014
迈克尔指出了一个巨大的“陷阱”,我刚才不得不处理同样的事情。我发现避免它的最简单方法是将 for() 循环分解为 while() 并小心递增
10赞 Mykola Golubyev 2/28/2009 #3

使用算法。std::remove_if

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

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

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

  1. std::remove_if.或者 ( 如果您知道您使用的是 list 而不是list::remove_ifTCollection )
  2. std::for_each

评论

2赞 Brian Neal 2/28/2009
std::list 有一个remove_if成员函数,它比 remove_if 算法更有效(并且不需要“删除-擦除”习惯用语)。
0赞 Antonio 6/23/2021
@brianneal 这取决于您是否可以访问 C++20 功能,否则 C++17 仅提供 std::remove_if en.cppreference.com/w/cpp/algorithm/remove
1赞 Brian Neal 6/25/2021
@Antonio 我说的是 std::list 的remove_if成员函数。我已经离开C++很长时间了,但是当我写下我的评论时(超过11年前!),这是一回事,我很确定它仍然是。cplusplus.com/reference/list/list/remove_if
0赞 Antonio 6/28/2021
@BrianNeal 是的,我不知道我误解了你的评论,这实际上很清楚。
2赞 anand 2/28/2009 #4

删除仅使指向被删除元素的迭代器失效。

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

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

评论

2赞 Brian Neal 2/28/2009
使用后增量要优雅得多。
28赞 Mike 2/28/2009 #5

您需要将 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);
    }
}

当然,最高效、最酷®的 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)));

评论

1赞 AShelly 2/28/2009
我确实考虑过您的 SuperCool 方法,我的犹豫是,对 remove_if 的调用并没有明确说明目标是处理项目,而不是将它们从活动项目列表中删除。(这些项目不会被删除,因为它们只是变得不活跃,而不是不需要)
1赞 Mike 3/2/2009
我想你是对的。一方面,我倾向于建议更改“update”的名称以消除晦涩难懂,但事实是,这段代码对函子来说很花哨,但它也不是晦涩难懂的。
0赞 Mike 4/13/2017
公平评论,要么修复 while 循环以使用 end,要么删除未使用的定义。
0赞 luizfls 9/3/2021
旁注:并已被弃用。std::not1std::mem_fun
5赞 Rafael Gago 8/5/2011 #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赞 trololo 2/11/2013
这也是我用过的。但是我不确定如果要删除的元素是容器中的第一个元素,它是否能保证工作。对我来说,我认为它有效,但我不确定它是否可跨平台移植。
0赞 milesma 7/26/2013
我没有做“-1”,但是,列表迭代器不能递减吗?至少我从 Visual Studio 2008 那里得到了断言。
0赞 Rafael Gago 3/26/2014
只要链表实现为具有 head/stub 节点的圆形双链表(用作 end()、rbegin(),当空时也用作 begin() 和 rend(),这将起作用。我不记得我在哪个平台上使用它,但它也对我有用,因为上面提到的实现是 std::list 最常见的实现。但无论如何,几乎可以肯定这是在利用一些未定义的(根据 C++ 标准)行为,所以最好不要使用它。
0赞 Jesse Chisholm 4/25/2019
re:该方法需要一个 .某些集合实现提供了导致断言的 a。iterator cannot be decrementederaserandom access iteratorforward only iterator
0赞 Rafael Gago 12/30/2019
@Jesse Chisholm 来说,问题是关于 std::list 的,而不是一个任意的容器。std::list 提供擦除和双向迭代器。
-4赞 Marcin Skoczylas 9/19/2012 #7

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

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赞 Jesse Chisholm 4/25/2019
我猜这是对风格偏好的否决,因为它似乎是功能性的。是的,当然,(1)它使用了一个额外的迭代器,(2)迭代器增量在一个奇怪的循环位置,没有充分的理由把它放在那里,(3)它在决定删除之后而不是像OP中那样在之前进行通道工作。 但这不是一个错误的答案。
5赞 David Cormack 7/5/2013 #8

下面是一个使用循环的示例,该循环循环访问列表,并在遍历列表期间删除项时递增或重新验证迭代器。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);
2赞 Alex Bagg 6/4/2015 #9

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

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。

评论

0赞 sg7 4/9/2018
有趣的方法,但恐怕它可能会很慢。
1赞 J. Kalz 3/26/2016 #10

你可以写

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

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

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;
}));
11赞 Jayhello 10/24/2018 #11

我总结了一下,这里是三种方法的例子:

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_iferase

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 

评论

1赞 sklott 5/25/2021
在 C++20 中,您只能使用 .它与选项 2 和 3 基本相同,但更短,适用于任何类型的容器。std::erase_if(lst, pred)
2赞 sancho.s ReinstateMonicaCellio 3/13/2019 #12

向后迭代可避免擦除要遍历的其余元素上的元素的影响:

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:请参阅,例如,关于向后迭代。

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

评论

0赞 Jesse Chisholm 4/25/2019
回复:对于一个列表,可能是的。对于向量来说,可能不是。这在任意集合中是无法保证的。例如,地图可能会决定重新平衡自身。avoids the effect of erasing an element on the remaining elements
0赞 Oliver Stieber 7/13/2020 #13

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

评论

0赞 AShelly 7/14/2020
这是低效的 - 每次删除列表时,它都会从头开始重新启动列表。
0赞 mach6 5/8/2021 #14

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

#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;
}
0赞 rherrmannr 4/25/2023 #15

在 C++20 中,您可以使用erease_if

std::erease_if(items, [](auto& i){ 
  if (!i.update()) {
    return true;
  }
  other_code_involving(i);
  return false;
};