迭代时从std :: set中删除元素


147

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

这是我编写的测试代码:

#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内有多个测试条件,则每个条件必须增加迭代器。我更喜欢此代码,因为迭代器仅在一个位置递增,从而使代码不易出错且可读性更高。



3
实际上,我在问我的问题之前就读过这个问题(和其他问题),但是由于它们与其他STL容器有关,并且由于我的最初测试显然可行,因此我认为它们之间存在一些差异。只有在Matt回答之后,我才想到使用valgrind。即使,我更喜欢我的NEW解决方案,因为它通过仅在一个位置增加迭代器来减少出错的机会。谢谢大家的帮助!
pedromanoel

1
@pedromanoel ++it应该比it++它更有效,因为它不需要使用迭代器的不可见临时副本。Kornel版本更长,可确保最有效地迭代未过滤的元素。
Alnitak 2012年

@Alnitak我还没有考虑过,但是我认为性能上的差别不会太大。副本也以他的版本创建,但仅针对匹配的元素。因此,优化程度完全取决于集合的结构。在相当长的一段时间里,我对代码进行了预优化,从而在过程中损害了可读性和编码速度。因此,在使用其他方法之前,我将进行一些测试。
pedromanoel 2012年

Answers:


178

这取决于实现:

标准23.1.2.8:

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

也许您可以尝试一下-这是符合标准的:

for (auto it = numbers.begin(); it != numbers.end(); ) {
    if (*it % 2 == 0) {
        numbers.erase(it++);
    }
    else {
        ++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;
    }
}

2
这不适deque 用于MSVC2013。它们的实现是有问题的,还是有另一个要求阻止它的进行deque。STL规范非常复杂,以至于您不能期望所有的实现都遵循它,更不用说您的临时程序员了。STL是一个无法驯服的怪兽,并且由于没有独特的实现方式(并且测试套件(如果有的话,显然没有涵盖删除循环中的元素的明显情况)),这使STL成为了一个闪亮的,易碎的玩具当您侧身看时会发出一声巨响。
kuroi neko

@MatthieuM。它在C ++ 11中实现。在C ++ 17中,现在需要迭代器(在C ++ 11中为const_iterator)。
tartaruga_casco_mole

19

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

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())
iammilind

7

您误解了“不确定行为”的含义。未定义的行为并不表示“如果执行此操作,则程序崩溃或产生意外结果”。这意味着“如果执行此操作,则程序可能会崩溃或产生意外结果”,或执行其他任何操作,具体取决于您的编译器,操作系统,月相等。

如果某件事执行时没有崩溃并且表现出预期的效果,则不能证明这不是未定义的行为。它所证明的是,在特定操作系统上使用特定编译器进行编译后,其行为恰好符合该特定运行所观察到的。

从集合中删除一个元素会使迭代器对被删除的元素无效。使用无效的迭代器是未定义的行为。碰巧的是,观察到的行为正是您在此特定情况下想要的。这并不意味着该代码是正确的。


哦,我很清楚,未定义的行为也可能意味着“它对我有用,但对每个人都不起作用”。这就是为什么我问这个问题,因为我不知道这种行为是否正确。如果是这样,那我就那样离开。使用while循环可以解决我的问题,然后呢?我用建议的解决方案编辑了问题。请检查一下。
pedromanoel 2010年

它也对我有用。但是,当我将条件更改为if (n > 2 && n < 7 )0时,我得到0 1 2 4 7 89。-这里的特定结果可能更多地取决于擦除方法和设置迭代器的实现细节,而不是取决于月亮的相位(不是那个应该依靠实施细节)。;)
UncleBens

1
STL为“未定义的行为”添加了许多新的含义。例如“ Microsoft认为可以通过允许std::set::erase返回迭代器来增强规格,因此,由gcc编译时,您的MSVC代码会爆炸”,或“ Microsoft进行了边界检查,std::bitset::operator[]因此您精心优化的位集算法将变慢到使用MSVC编译时进行抓取”。STL没有独特的实现,它的规范是呈指数级增长的肿混乱,因此难怪从循环内部删除元素需要高级程序员专业知识...
kuroi neko 2015年

2

只是警告一下,在使用双端队列容器的情况下,所有检查双端队列迭代器与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()
杰西·奇斯霍尔姆

2

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

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

并且,将工作为vectorsetdeque,等见cppReference获取更多信息。


1

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


1
Set<T>::erase版本不返回迭代器。
Arkaitz Jimenez

4
确实可以,但是仅在MSVC实现上。因此,这确实是特定于实现的答案。:)
尤金(Eugene)2012年

1
@Eugene它适用于所有使用C ++ 11的实现
mastov

gcc 4.8with的某些实现c++1y在擦除中存在错误。 it = collection.erase(it);应该可以使用,但使用起来可能更安全collection.erase(it++);
Jesse Chisholm

1

我认为使用STL方法remove_if尝试删除由迭代器包装的对象时, 'from可以帮助防止某些奇怪的问题。

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

假设我们有某种容器,例如vector或称为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'方法。


0

我遇到了同样的老问题,发现下面的代码更容易理解,这与上面的解决方案有所不同。

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是关于有选择地擦除项目并仍然具有有效的迭代器。
杰西·奇斯霍尔姆
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.