【问题标题】:Deleting elements from std::set while iterating迭代时从 std::set 中删除元素
【发布时间】:2011-02-21 21:34:17
【问题描述】:

我需要检查一组并删除符合预定义条件的元素。

这是我写的测试代码:

#include <set>
#include <algorithm>

void printElement(int value) {
    std::cout << value << " ";
}

int main() {
    int initNum[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    std::set<int> numbers(initNum, initNum + 10);
    // print '0 1 2 3 4 5 6 7 8 9'
    std::for_each(numbers.begin(), numbers.end(), printElement);

    std::set<int>::iterator it = numbers.begin();

    // iterate through the set and erase all even numbers
    for (; it != numbers.end(); ++it) {
        int n = *it;
        if (n % 2 == 0) {
            // wouldn't invalidate the iterator?
            numbers.erase(it);
        }
    }

    // print '1 3 5 7 9'
    std::for_each(numbers.begin(), numbers.end(), printElement);

    return 0;
}

起初,我认为在迭代时从集合中删除一个元素会使迭代器无效,并且 for 循环中的增量会产生未定义的行为。尽管如此,我执行了这个测试代码并且一切顺利,我无法解释为什么。

我的问题: 这是标准集的定义行为还是此实现特定?顺便说一句,我在 ubuntu 10.04(32 位版本)上使用 gcc 4.3.3。

谢谢!

建议的解决方案:

这是从集合中迭代和擦除元素的正确方法吗?

while(it != numbers.end()) {
    int n = *it;
    if (n % 2 == 0) {
        // post-increment operator returns a copy, then increment
        numbers.erase(it++);
    } else {
        // pre-increment operator increments, then return
        ++it;
    }
}

编辑:首选解决方案

我想出了一个对我来说似乎更优雅的解决方案,即使它完全一样。

while(it != numbers.end()) {
    // copy the current iterator then increment it
    std::set<int>::iterator current = it++;
    int n = *current;
    if (n % 2 == 0) {
        // don't invalidate iterator it, because it is already
        // pointing to the next element
        numbers.erase(current);
    }
}

如果 while 内有多个测试条件,则每个条件都必须递增迭代器。我更喜欢这段代码,因为迭代器只在一个地方递增,使代码不易出错且更具可读性。

【问题讨论】:

  • 实际上,我在问我之前阅读了这个问题(和其他问题),但由于它们与其他 STL 容器有关,而且我的初始测试显然有效,我认为它们之间存在一些差异。只有在马特的回答之后,我才想到使用 valgrind。尽管如此,我更喜欢我的新解决方案而不是其他解决方案,因为它通过仅在一个地方增加迭代器来减少出错的机会。谢谢大家的帮助!
  • @pedromanoel ++it 应该比it++ 更有效,因为它不需要使用不可见的迭代器临时副本。 Kornel 的版本虽然更长,但可以确保最有效地迭代未过滤的元素。
  • @Alnitak 我没有考虑过这一点,但我认为性能差异不会那么大。该副本也在他的版本中创建,但仅适用于匹配的元素。所以优化程度完全取决于集合的结构。在相当长的一段时间里,我预先优化了代码,在这个过程中损害了可读性和编码速度......所以我会在使用其他方式之前进行一些测试。

标签: c++ iterator set std c++-standard-library


【解决方案1】:

这取决于实现:

标准 23.1.2.8:

插入成员不会影响迭代器和对容器的引用的有效性,而擦除成员只会使迭代器和对被擦除元素的引用无效。

也许你可以试试这个——这是符合标准的:

for (auto it = numbers.begin(); it != numbers.end(); ) {
    if (*it % 2 == 0) {
        numbers.erase(it++);
    }
    else {
        ++it;
    }
}

注意it++是后缀,因此它通过旧位置擦除,但由于运算符的原因首先跳转到新位置。

2015.10.27 更新: C++11 已经解决了这个缺陷。 iterator erase (const_iterator position); 将迭代器返回到最后一个被删除元素之后的元素(或set::end,如果最后一个元素被删除)。所以C++11的风格是:

for (auto it = numbers.begin(); it != numbers.end(); ) {
    if (*it % 2 == 0) {
        it = numbers.erase(it);
    }
    else {
        ++it;
    }
}

【讨论】:

  • 这不适用于 MSVC2013 上的 deque。要么他们的实现有问题,要么还有另一个要求阻止它在deque 上工作。 STL 规范非常复杂,你不能指望所有的实现都遵循它,更不用说你的普通程序员记住它了。 STL 是一个无法驯服的怪物,并且由于没有独特的实现(并且测试套件,如果有的话,显然不包括删除循环中的元素这样明显的情况),这使得 STL 成为一个闪亮的易碎玩具,可以与当你从侧面看时会发出一声巨响。
  • @MatthieuM。它在 C++11 中实现。在 C++17 中,它现在需要迭代器(C++11 中的 const_iterator)。
  • @kuroineko 这对双端队列不起作用,因为erase invalidate all iterator
  • (指1st sn-p,以历史顺序判断)
【解决方案2】:

如果你通过 valgrind 运行你的程序,你会看到一堆读取错误。换句话说,是的,迭代器正在失效,但是您在示例中很幸运(或者真的很不幸,因为您没有看到未定义行为的负面影响)。一种解决方案是创建一个临时迭代器,增加 temp,删除目标迭代器,然后将目标设置为 temp。例如,重写你的循环如下:

std::set<int>::iterator it = numbers.begin();                               
std::set<int>::iterator tmp;                                                

// iterate through the set and erase all even numbers                       
for ( ; it != numbers.end(); )                                              
{                                                                           
    int n = *it;                                                            
    if (n % 2 == 0)                                                         
    {                                                                       
        tmp = it;                                                           
        ++tmp;                                                              
        numbers.erase(it);                                                  
        it = tmp;                                                           
    }                                                                       
    else                                                                    
    {                                                                       
        ++it;                                                               
    }                                                                       
} 

【讨论】:

  • 如果只有条件重要且不需要范围内初始化或后期操作,那么最好使用while 循环。即for ( ; it != numbers.end(); ) 使用while (it != numbers.end()) 可见性更好
【解决方案3】:

您误解了“未定义行为”的含义。未定义的行为并不意味着“如果您这样做,您的程序崩溃或产生意外结果。”它的意思是“如果你这样做,你的程序可能崩溃或产生意想不到的结果”,或者做任何其他事情,这取决于你的编译器、你的操作系统、月相等。

如果某些东西在没有崩溃的情况下执行并且行为符合您的预期,则不是证明它不是未定义的行为。它所证明的只是,在特定操作系统上使用特定编译器进行编译后,它的行为恰好与特定运行所观察到的一样。

从集合中擦除元素会使指向被擦除元素的迭代器无效。使用无效的迭代器是未定义的行为。碰巧观察到的行为是您在这个特定实例中想要的。这并不意味着代码是正确的。

【讨论】:

  • 哦,我很清楚未定义的行为也可能意味着“它适用于我,但不适用于所有人”。这就是我问这个问题的原因,因为我不知道这种行为是否正确。如果是的话,那我就这样离开了。那么使用while循环可以解决我的问题吗?我用我提出的解决方案编辑了我的问题。请检查一下。
  • 它也适用于我。但是当我将条件更改为if (n &gt; 2 &amp;&amp; n &lt; 7 ) 时,我得到 0 1 2 4 7 8 9。 - 这里的特定结果可能更多地取决于擦除方法和设置迭代器的实现细节,而不是关于月相(不应该依赖实施细节)。 ;)
  • STL 为“未定义的行为”添加了许多新含义。例如,“微软认为通过允许 std::set::erase 返回一个迭代器来增强规范是明智的,所以当你的 MSVC 代码被 gcc 编译时,你的 MSVC 代码会飞起来”,或者“微软会对 std::bitset::operator[] 进行绑定检查,所以你仔细优化使用 MSVC 编译时,bitset 算法会慢到爬行”。 STL 没有唯一的实现,它的规范是一个指数级增长的臃肿的混乱,所以难怪从循环内删除元素需要高级程序员的专业知识......
【解决方案4】:

C++20 将具有“统一容器擦除”,您将能够编写:

std::erase_if(numbers, [](int n){ return n % 2 == 0 });

这适用于vectorsetdeque 等。 请参阅cppReference 了解更多信息。

【讨论】:

    【解决方案5】:

    提醒一下,在双端队列容器的情况下,所有检查双端队列迭代器是否与 numbers.end() 相等的解决方案都可能在 gcc 4.8.4 上失败。也就是说,擦除双端队列的元素通常会使指向 numbers.end() 的指针无效:

    #include <iostream>
    #include <deque>
    
    using namespace std;
    int main() 
    {
    
      deque<int> numbers;
    
      numbers.push_back(0);
      numbers.push_back(1);
      numbers.push_back(2);
      numbers.push_back(3);
      //numbers.push_back(4);
    
      deque<int>::iterator  it_end = numbers.end();
    
      for (deque<int>::iterator it = numbers.begin(); it != numbers.end(); ) {
        if (*it % 2 == 0) {
          cout << "Erasing element: " << *it << "\n";
          numbers.erase(it++);
          if (it_end == numbers.end()) {
        cout << "it_end is still pointing to numbers.end()\n";
          } else {
        cout << "it_end is not anymore pointing to numbers.end()\n";
          }
        }
        else {
          cout << "Skipping element: " << *it << "\n";
          ++it;
        }
      }
    }
    

    输出:

    Erasing element: 0
    it_end is still pointing to numbers.end()
    Skipping element: 1
    Erasing element: 2
    it_end is not anymore pointing to numbers.end()
    

    请注意,虽然双端队列转换在这种特殊情况下是正确的,但在此过程中结束指针已失效。使用不同大小的双端队列,错误更加明显:

    int main() 
    {
    
      deque<int> numbers;
    
      numbers.push_back(0);
      numbers.push_back(1);
      numbers.push_back(2);
      numbers.push_back(3);
      numbers.push_back(4);
    
      deque<int>::iterator  it_end = numbers.end();
    
      for (deque<int>::iterator it = numbers.begin(); it != numbers.end(); ) {
        if (*it % 2 == 0) {
          cout << "Erasing element: " << *it << "\n";
          numbers.erase(it++);
          if (it_end == numbers.end()) {
        cout << "it_end is still pointing to numbers.end()\n";
          } else {
        cout << "it_end is not anymore pointing to numbers.end()\n";
          }
        }
        else {
          cout << "Skipping element: " << *it << "\n";
          ++it;
        }
      }
    }
    

    输出:

    Erasing element: 0
    it_end is still pointing to numbers.end()
    Skipping element: 1
    Erasing element: 2
    it_end is still pointing to numbers.end()
    Skipping element: 3
    Erasing element: 4
    it_end is not anymore pointing to numbers.end()
    Erasing element: 0
    it_end is not anymore pointing to numbers.end()
    Erasing element: 0
    it_end is not anymore pointing to numbers.end()
    ...
    Segmentation fault (core dumped)
    

    这是解决此问题的方法之一:

    #include <iostream>
    #include <deque>
    
    using namespace std;
    int main() 
    {
    
      deque<int> numbers;
      bool done_iterating = false;
    
      numbers.push_back(0);
      numbers.push_back(1);
      numbers.push_back(2);
      numbers.push_back(3);
      numbers.push_back(4);
    
      if (!numbers.empty()) {
        deque<int>::iterator it = numbers.begin();
        while (!done_iterating) {
          if (it + 1 == numbers.end()) {
        done_iterating = true;
          } 
          if (*it % 2 == 0) {
        cout << "Erasing element: " << *it << "\n";
          numbers.erase(it++);
          }
          else {
        cout << "Skipping element: " << *it << "\n";
        ++it;
          }
        }
      }
    }
    

    【讨论】:

    • 密钥是do not trust an old remembered dq.end() value, always compare to a new call to dq.end()
    【解决方案6】:

    此行为是特定于实现的。为了保证迭代器的正确性,您应该使用“it = numbers.erase(it);”如果您需要删除元素并在其他情况下简单地增加迭代器,则声明。

    【讨论】:

    • Set&lt;T&gt;::erase 版本不返回迭代器。
    • 实际上确实如此,但仅限于 MSVC 实现。所以这确实是一个特定于实现的答案。 :)
    • @Eugene 它适用于所有使用 C++11 的实现
    • gcc 4.8c++1y 的某些实现在擦除中有错误。 it = collection.erase(it); 应该可以工作,但使用 collection.erase(it++); 可能更安全
    【解决方案7】:

    我认为使用 STL 方法 'remove_if' 可以帮助防止在尝试删除被迭代器包装的对象时出现一些奇怪的问题。

    此解决方案可能效率较低。

    假设我们有某种容器,例如向量或称为 m_bullets 的列表:

    Bullet::Ptr is a shared_pr<Bullet>
    

    'it' 是 'remove_if' 返回的迭代器,第三个参数是对容器的每个元素执行的 lambda 函数。因为容器包含Bullet::Ptr,所以 lambda 函数需要获取该类型(或对该类型的引用)作为参数传递。

     auto it = std::remove_if(m_bullets.begin(), m_bullets.end(), [](Bullet::Ptr bullet){
        // dead bullets need to be removed from the container
        if (!bullet->isAlive()) {
            // lambda function returns true, thus this element is 'removed'
            return true;
        }
        else{
            // in the other case, that the bullet is still alive and we can do
            // stuff with it, like rendering and what not.
            bullet->render(); // while checking, we do render work at the same time
            // then we could either do another check or directly say that we don't
            // want the bullet to be removed.
            return false;
        }
    });
    // The interesting part is, that all of those objects were not really
    // completely removed, as the space of the deleted objects does still 
    // exist and needs to be removed if you do not want to manually fill it later 
    // on with any other objects.
    // erase dead bullets
    m_bullets.erase(it, m_bullets.end());
    

    'remove_if' 删除 lambda 函数返回 true 的容器,并将该内容移动到容器的开头。 'it' 指​​向一个可被视为垃圾的未定义对象。从 'it' 到 m_bullets.end() 的对象可以被擦除,因为它们占用内存,但包含垃圾,因此在该范围内调用 'erase' 方法。

    【讨论】:

      【解决方案8】:

      我遇到了同样的老问题,发现下面的代码更可理解,这在某种程度上符合上述解决方案。

      std::set<int*>::iterator beginIt = listOfInts.begin();
      while(beginIt != listOfInts.end())
      {
          // Use your member
          std::cout<<(*beginIt)<<std::endl;
      
          // delete the object
          delete (*beginIt);
      
          // erase item from vector
          listOfInts.erase(beginIt );
      
          // re-calculate the begin
          beginIt = listOfInts.begin();
      }
      

      【讨论】:

      • 这只有在您总是删除每个项目时才有效。 OP 是关于有选择地擦除项目并仍然具有有效的迭代器。
      猜你喜欢
      • 2014-01-04
      • 2011-03-22
      • 2019-01-15
      • 2010-10-10
      • 1970-01-01
      • 2015-06-30
      相关资源
      最近更新 更多