Мне нужно пройти через набор и удалить элементы, которые соответствуют заранее определенным критериям.
Это тестовый код, который я написал:
#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);
}
}
Если в то время как есть несколько условий теста, каждое из них должно увеличивать итератор. Мне больше нравится этот код, потому что итератор увеличивается только в одном месте , что делает код менее подверженным ошибкам и более читаемым.
++it
должен быть несколько более эффективным, чемit++
потому, что он не требует использования невидимой временной копии итератора. Версия Корнеля, хотя и дольше гарантирует, что нефильтрованные элементы будут проходить итерацию наиболее эффективно.Ответы:
Это зависит от реализации:
Стандарт 23.1.2.8:
Может быть, вы могли бы попробовать это - это стандартное соответствие:
Обратите внимание, что это ++ является постфиксом, поэтому он передает старую позицию для удаления, но сначала переходит к новой позиции из-за оператора.
2015.10.27 обновление: C ++ 11 устраняет дефект.
iterator erase (const_iterator position);
вернуть итератор в элемент, следующий за последним удаленным элементом (илиset::end
, если последний элемент был удален). Итак, стиль C ++ 11:источник
deque
MSVC2013. Либо их реализация содержит ошибки, либо существует еще одно требование, которое мешает этому работатьdeque
. Спецификация STL настолько сложна, что вы не можете ожидать, что все реализации будут следовать ей, не говоря уже о том, что ваш программист запомнит ее. STL - чудовище, за пределами укрощения, и поскольку нет уникальной реализации (и тестовые наборы, если таковые имеются, очевидно, не охватывают такие очевидные случаи, как удаление элементов в цикле), что делает STL блестящей хрупкой игрушкой, которая может подняться удар, когда вы смотрите на него вбок.Если вы запустите свою программу через valgrind, вы увидите кучу ошибок чтения. Другими словами, да, итераторы становятся недействительными, но вам повезло в вашем примере (или действительно не повезло, так как вы не видите негативных последствий неопределенного поведения). Одним из решений этой проблемы является создание временного итератора, увеличение временного значения, удаление целевого итератора, а затем установка целевого значения для временного. Например, переписать ваш цикл следующим образом:
источник
while
цикл. тоfor ( ; it != numbers.end(); )
есть лучше видно сwhile (it != numbers.end())
Вы неправильно понимаете, что означает «неопределенное поведение». Неопределенное поведение не означает «если вы сделаете это, ваша программа потерпит крах или даст неожиданные результаты». Это означает «если вы сделаете это, ваша программа может произойти сбой или привести к неожиданным результатам», или сделать что-нибудь еще, в зависимости от вашего компилятора, вашей операционной системы, фазы луны и т. Д.
Если что-то выполняется без сбоев и ведет себя так, как вы ожидаете, это не является доказательством того, что это не неопределенное поведение. Все, что он доказывает, это то, что его поведение оказалось таким же, как наблюдается для этого конкретного прогона после компиляции с этим конкретным компилятором в этой конкретной операционной системе.
Стирание элемента из набора делает итератор стертым элементом недействительным. Использование недействительного итератора - неопределенное поведение. Так уж случилось, что наблюдаемое поведение было тем, что вы намеревались в данном конкретном случае; это не значит, что код правильный.
источник
if (n > 2 && n < 7 )
тогда, я получаю 0 1 2 4 7 8 9. - Конкретный результат здесь, вероятно, больше зависит от деталей реализации метода стирания и итераторов набора, а не от фазы луны (не той следует когда-либо полагаться на детали реализации). ;)std::set::erase
возвращать итератор, чтобы ваш код MSVC работал с треском при компиляции с помощью gcc», или «Microsoft выполняет привязанные проверки,std::bitset::operator[]
поэтому ваш тщательно оптимизированный алгоритм набора битов замедлится до сканировать при компиляции с MSVC ". У STL нет уникальной реализации, и его спецификация - экспоненциально растущий раздутый беспорядок, поэтому неудивительно, что удаление элементов из цикла требует опыта старшего программиста ...Просто чтобы предупредить, что в случае контейнера deque все решения, проверяющие равенство deque итератора к numbers.end (), вероятно, потерпят неудачу на gcc 4.8.4. А именно, удаление элемента deque обычно делает недействительным указатель на numbers.end ():
Вывод:
Обратите внимание, что, хотя преобразование deque является правильным в данном конкретном случае, указатель конца был недействительным на этом пути. С deque другого размера ошибка более очевидна:
Вывод:
Вот один из способов исправить это:
источник
do not trust an old remembered dq.end() value, always compare to a new call to dq.end()
.C ++ 20 будет иметь «равномерное стирание контейнера», и вы сможете написать:
И это будет работать
vector
,set
,deque
и т.д. См cppReference для получения дополнительной информации.источник
Это поведение зависит от реализации. Чтобы гарантировать правильность итератора, вы должны использовать «it = numbers.erase (it);» Заявление, если вам нужно удалить элемент и просто incerement итератор в другом случае.
источник
Set<T>::erase
версия не возвращает итератор.gcc 4.8
сc++1y
ошибкой стереть.it = collection.erase(it);
должен работать, но это может быть более безопасно для использованияcollection.erase(it++);
Я думаю, используя метод STL
remove_if
' может помочь предотвратить некоторые странные проблемы при попытке удалить объект, который обернут итератором.Это решение может быть менее эффективным.
Допустим, у нас есть какой-то контейнер, например, vector или список с именем m_bullets:
'
it
' - это итератор, который 'remove_if
' возвращает, третий аргумент - это лямбда-функция, которая выполняется для каждого элемента контейнера. Поскольку контейнер содержитBullet::Ptr
, лямбда-функция должна получить этот тип (или ссылку на этот тип) в качестве аргумента.'
remove_if
' удаляет контейнер, в котором лямбда-функция вернула истину, и перемещает это содержимое в начало контейнера. Символ 'it
' указывает на неопределенный объект, который можно считать мусором. Объекты от 'it' до m_bullets.end () могут быть удалены, так как они занимают память, но содержат мусор, поэтому в этом диапазоне вызывается метод 'erase'.источник
Я натолкнулся на ту же старую проблему и обнаружил, что приведенный ниже код более понятен, что в некоторой степени соответствует вышеуказанным решениям.
источник