Удаление элементов из std :: set во время итерации

Мне нужно пройти через набор и удалить элементы, которые соответствуют заранее определенным критериям.

Это тестовый код, который я написал:

#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 будет иметь неопределенное поведение. Несмотря на это, я выполнил этот тестовый код, и все прошло хорошо, и я не могу объяснить, почему.

Мой вопрос: это определенное поведение для стандартных наборов данных или эта реализация зависит от конкретной реализации? Кстати, я использую gcc 4.3.3 на ubuntu 10.04 (32-разрядная версия).

Спасибо!

Предлагаемое решение:

Это правильный способ перебора и удаления элементов из набора?

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 есть несколько тестовых условий, каждое из них должно увеличивать итератор. Мне больше нравится этот код, потому что итератор увеличивается только в одном месте, что делает код менее подверженным ошибкам и более читабельным.


person pedromanoel    schedule 20.05.2010    source источник
comment
На вопрос и ответ: stackoverflow.com/questions/263945/   -  person Martin York    schedule 20.05.2010
comment
На самом деле, я прочитал этот (и другие) вопрос, прежде чем задать свой, но поскольку они были связаны с другими контейнерами STL и поскольку мой первоначальный тест, по-видимому, сработал, я подумал, что между ними есть некоторая разница. Только после ответа Мэтта я подумал об использовании valgrind. Тем не менее, я предпочитаю свое НОВОЕ решение другим, потому что оно снижает вероятность ошибок за счет увеличения итератора только в одном месте. Спасибо всем за помощь!   -  person pedromanoel    schedule 06.07.2010
comment
@pedromanoel ++it должен быть несколько более эффективным, чем it++, потому что он не требует использования невидимой временной копии итератора. Версия Корнеля, хотя и более длинная, обеспечивает наиболее эффективное повторение нефильтрованных элементов.   -  person Alnitak    schedule 22.10.2012
comment
@Alnitak Я не думал об этом, но думаю, что разница в производительности будет не такой уж большой. Копия создается и в его версии, но только для совпадающих элементов. Таким образом, степень оптимизации полностью зависит от структуры набора. В течение некоторого времени я предварительно оптимизировал код, ухудшая читаемость и скорость кодирования в процессе ... Поэтому я бы провел несколько тестов, прежде чем использовать другой способ.   -  person pedromanoel    schedule 22.10.2012
comment
возможный дубликат Можно ли удалить элементы из стандартного :: list во время итерации?   -  person bobobobo    schedule 10.12.2012
comment
Является ли исходная проблема, которую вы пытаетесь решить, простым удалением элементов (из набора), которые соответствуют заранее определенным критериям? Потому что для этого вам даже не нужна итерация, что сделало бы более конкретные вопросы о стирании при повторении излишними. Но если вам нужно стереть во время итерации, я не могу вам помочь. :)   -  person Jeremy W. Murphy    schedule 09.12.2013
comment
Вау ... Я задал этот вопрос так давно, что не помню свою первоначальную проблему! Что ты имеешь в виду, мне не нужна итерация в таком случае?   -  person pedromanoel    schedule 18.12.2013
comment
Извините, неважно, у меня возникло недопонимание по поводу STL. :)   -  person Jeremy W. Murphy    schedule 05.01.2014
comment
У меня проблемы с вашим предпочтительным решением, кажется, у меня бесконечный цикл. Я использую deque, а не set, однако остальная часть моего кода является минимальным тестовым примером для вашего предлагаемого метода ...   -  person Troyseph    schedule 11.08.2015


Ответы (8)


Это зависит от реализации:

Стандарт 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;
    }
}
person Kornel Kisielewicz    schedule 20.05.2010
comment
Это не работает с deque на MSVC2013. Либо их реализация ошибочна, либо есть еще одно требование, которое не позволяет этому работать с deque. Спецификация STL настолько запутана, что вы не можете ожидать, что все реализации будут следовать ей, не говоря уже о том, чтобы ваш случайный программист запомнил ее. STL - это чудовище, которое невозможно приручить, и поскольку нет уникальной реализации (и наборы тестов, если таковые имеются, по-видимому, не охватывают такие очевидные случаи, как удаление элементов в цикле), это делает STL блестящей хрупкой игрушкой, которая может расти с челка, когда смотришь на нее сбоку. - person kuroi neko; 30.01.2015
comment
@MatthieuM. Это есть в C ++ 11. В C ++ 17 теперь требуется итератор (const_iterator в C ++ 11). - person tartaruga_casco_mole; 23.01.2018
comment
@kuroineko, это не сработает на deque, потому что erase аннулирует весь итератор - person apple apple; 14.07.2021
comment
(относится к 1-му фрагменту, судя по порядку истории) - person apple apple; 14.07.2021

Если вы запустите свою программу через 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;                                                               
    }                                                                       
} 
person Matt    schedule 20.05.2010
comment
Если имеет значение только условие и не требует инициализации в области видимости или постоперации, тогда лучше использовать цикл while. т.е. for ( ; it != numbers.end(); ) лучше видно с while (it != numbers.end()) - person iammilind; 28.04.2018

Вы неправильно понимаете, что означает «неопределенное поведение». Неопределенное поведение не означает, что «если вы сделаете это, ваша программа будет аварийно завершена или выдаст неожиданные результаты». Это означает «если вы сделаете это, ваша программа может дать сбой или дать неожиданные результаты» или сделать что-нибудь еще, в зависимости от вашего компилятора, вашей операционной системы, фазы луны и т. Д.

Если что-то выполняется без сбоев и ведет себя так, как вы ожидаете, это не доказывает, что это не неопределенное поведение. Все, что он доказывает, - это то, что его поведение соответствовало наблюдаемому для этого конкретного запуска после компиляции с этим конкретным компилятором в этой конкретной операционной системе.

Удаление элемента из набора делает недействительным итератор для стертого элемента. Использование недействительного итератора - неопределенное поведение. Так уж получилось, что наблюдаемое поведение было тем, что вы планировали в данном конкретном случае; это не значит, что код правильный.

person Tyler McHenry    schedule 20.05.2010
comment
О, я прекрасно понимаю, что неопределенное поведение также может означать, что оно работает для меня, но не для всех. Вот почему я задал этот вопрос, потому что я не знал, правильно ли это поведение или нет. Если бы это было так, я бы просто так ушел. Тогда использование цикла while решило бы мою проблему? Я отредактировал свой вопрос своим предложенным решением. Пожалуйста, проверьте это. - person pedromanoel; 20.05.2010
comment
У меня тоже работает. Но когда я меняю условие на if (n > 2 && n < 7 ), я получаю 0 1 2 4 7 8 9. - Конкретный результат здесь, вероятно, больше зависит от деталей реализации метода стирания и установки итераторов, а не от фаза луны (не то чтобы никогда не следует полагаться на детали реализации). ;) - person UncleBens; 20.05.2010
comment
STL добавляет много нового значения неопределенному поведению. Например, Microsoft сочла разумным усовершенствовать спецификацию, разрешив std::set::erase возвращать итератор, чтобы ваш код MSVC взлетел с треском при компиляции с помощью gcc, или Microsoft выполняет связанные проверки std::bitset::operator[], поэтому ваш тщательно оптимизированный алгоритм битового набора будет медленно сканировать. при компиляции с MSVC. STL не имеет уникальной реализации, а его спецификация представляет собой экспоненциально растущий беспорядок, поэтому неудивительно, что удаление элементов из цикла требует опыта старшего программиста ... - person kuroi neko; 30.01.2015

В C ++ 20 будет «равномерное стирание контейнера», и вы сможете написать:

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

И это будет работать для vector, set, deque и т. Д. См. cppReference для получения дополнительной информации.

person Marshall Clow    schedule 10.01.2019

Просто чтобы предупредить, что в случае контейнера двухсторонней очереди все решения, которые проверяют равенство двухсторонней очереди итератору number.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;
      }
    }
  }
}
person McKryak    schedule 23.08.2015
comment
Ключ - do not trust an old remembered dq.end() value, always compare to a new call to dq.end(). - person Jesse Chisholm; 25.04.2019

Это поведение зависит от реализации. Чтобы гарантировать правильность итератора, вы должны использовать "it = numbers.erase (it);" оператор, если вам нужно удалить элемент и просто включить итератор в другом случае.

person Vitaly Bogdanov    schedule 20.05.2010
comment
Set<T>::erase версия не возвращает итератор. - person Arkaitz Jimenez; 20.05.2010
comment
На самом деле это так, но только в реализации MSVC. Так что это действительно ответ для конкретной реализации. :) - person Eugene; 25.09.2012
comment
@Eugene Он делает это для всех реализаций с C ++ 11 - person mastov; 22.08.2017
comment
В некоторых реализациях gcc 4.8 с c++1y есть ошибка стирания. it = collection.erase(it); должен работать, но может быть безопаснее использовать collection.erase(it++); - person Jesse Chisholm; 25.04.2019

Я думаю, что использование метода STL 'remove_if' from могло бы помочь предотвратить некоторые странные проблемы при попытке удалить объект, обернутый итератором.

Это решение может быть менее эффективным.

Допустим, у нас есть какой-то контейнер, например вектор или список с именем m_bullets:

Bullet::Ptr is a shared_pr<Bullet>

«it» - это итератор, который возвращает «remove_if», третий аргумент - это лямбда-функция, которая выполняется для каждого элемента контейнера. Поскольку контейнер содержит Bullet::Ptr, лямбда-функции необходимо передать этот тип (или ссылку на этот тип) в качестве аргумента.

 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» удаляет контейнер, в котором лямбда-функция вернула истину, и перемещает это содержимое в начало контейнера. 'it' указывает на неопределенный объект, который можно рассматривать как мусор. Объекты от 'it' до m_bullets.end () могут быть удалены, поскольку они занимают память, но содержат мусор, поэтому для этого диапазона вызывается метод 'erase'.

person John Behm    schedule 10.01.2019

Я столкнулся с той же старой проблемой и обнаружил, что приведенный ниже код более понятен, что соответствует приведенным выше решениям.

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();
}
person Anurag    schedule 02.07.2018
comment
Это работает, только если вы всегда стираете каждый элемент. OP заключается в выборочном стирании элементов и сохранении действительных итераторов. - person Jesse Chisholm; 25.04.2019