Можете ли вы удалить элементы из списка std :: list, просматривая его?

239

У меня есть код, который выглядит так:

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

Я хотел бы удалить неактивные элементы сразу после их обновления, чтобы избежать повторного просмотра списка. Но если я добавляю закомментированные строки, я получаю сообщение об ошибке i++: «Итератор списка не может быть увеличен». Я пробовал несколько альтернатив, которые не увеличивались в выражении for, но я не мог заставить что-либо работать.

Какой лучший способ удалить элементы, когда вы ходите по стандартному списку?

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

Ответы:

286

Сначала нужно увеличить итератор (с помощью 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;
    }
}
Михаил Кристофик
источник
7
На самом деле, это не гарантирует работу. С помощью «erase (i ++);» мы знаем только то, что предварительно увеличенное значение передается в erase (), а значение i увеличивается перед точкой с запятой, необязательно перед вызовом erase (). "итератор prev = i ++; erase (prev);" обязательно будет работать, так как использовать возвращаемое значение
Джеймс Керран
58
Не Джеймс, i увеличивается до вызова стирания, а предыдущее значение передается функции. Аргументы функции должны быть полностью оценены перед вызовом функции.
Брайан Нил
28
@ Джеймс Керран: Это не правильно. ВСЕ аргументы полностью вычисляются перед вызовом функции.
Мартин Йорк,
9
Мартин Йорк прав. Все аргументы вызова функции полностью оцениваются перед вызовом функции, без исключений. Это просто, как работают функции. И это не имеет ничего общего с вашим примером foo.b (i ++). C (i ++) (который в любом случае не определен)
jalf
75
Альтернативное использование i = items.erase(i)более безопасно, поскольку оно эквивалентно списку, но все равно будет работать, если кто-то изменит контейнер на вектор. С вектором erase () перемещает все влево, чтобы заполнить отверстие. Если вы попытаетесь удалить последний элемент с кодом, который увеличивает итератор после стирания, конец перемещается влево, а итератор перемещается вправо - после конца. И тогда ты терпишь крах.
Эрик Сеппанен,
133

Вы хотите сделать:

i= items.erase(i);

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

MSN
источник
80
Имейте в виду, что вы не можете просто вставить этот код в цикл for. В противном случае вы пропустите элемент при каждом его удалении.
Майкл Кристофик
2
может ли он не делать я--; каждый раз, следуя его коду, чтобы не пропустить?
энтузиастик
1
@enthusiasticgeek, что будет, если i==items.begin()?
MSN
1
@enthusiasticgeek, в этот момент вы должны просто сделать i= items.erase(i);. Это каноническая форма, которая уже заботится обо всех этих деталях.
MSN
6
Майкл указывает на огромную «ошибку», мне пришлось иметь дело с тем же самым только сейчас. Самый простой способ избежать этого, который я нашел, - это просто разложить цикл for () на while () и быть осторожным с увеличивающимися значениями
Anne Quinn
22

Вам нужно сделать комбинацию ответа Кристо и 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);
    }
}

Конечно, самая эффективная и полезная вещь SuperCool® 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)));
Майк
источник
Я действительно рассмотрел ваш метод SuperCool, но я колебался, что вызов remove_if не дает понять, что цель состоит в том, чтобы обработать элементы, а не удалить их из списка активных. (Элементы не удаляются, потому что они становятся неактивными, а не ненужными)
AShelly
Я полагаю, вы правы. С одной стороны, я склонен предложить изменить имя «update», чтобы убрать неизвестность, но правда в том, что этот код причудлив с функторами, но он также не совсем неясен.
Майк
Справедливый комментарий: либо исправьте цикл while, чтобы использовать end, либо удалите неиспользуемое определение.
Майк
10

Используйте алгоритм std :: remove_if.

Редактировать: Работа с коллекциями должна быть такой: 1. Подготовить коллекцию. 2. процесс сбора.

Жизнь будет проще, если вы не будете смешивать эти шаги.

  1. станд :: remove_if. или list :: remove_if (если вы знаете, что работаете со списком, а не с TCollection)
  2. станд :: for_each
Николай Голубев
источник
2
std :: list имеет функцию-член remove_if, которая более эффективна, чем алгоритм remove_if (и не требует идиомы «remove-erase»).
Брайан Нил
5

Вот пример использования 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);
Дэвид Кормак
источник
4

Альтернатива петлевой версии ответу Кристо.

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

Ответ был совершенно не вовремя, я знаю ...

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
Это то, что я тоже использовал. Но я не уверен, гарантированно ли это работает, если удаляемый элемент является первым элементом в контейнере. Для меня это работает, я думаю, но я не уверен, что он переносим на разные платформы.
трололо
Я не делал "-1", однако, итератор списка не может уменьшаться? По крайней мере, я получил утверждение от Visual Studio 2008.
Milsma
До тех пор, пока связанный список реализован в виде круглого двойного связанного списка, имеющего узел head / stub (используется как end () rbegin (), а когда он пуст, используется также как begin () и rend ()), это будет работать. Я не могу вспомнить, на какой платформе я использовал это, но это работало и для меня, так как реализация, названная выше, является наиболее распространенной реализацией для std :: list. Но в любом случае, он почти уверен, что это использовало какое-то неопределенное (по стандарту C ++) поведение, поэтому лучше его не использовать.
Рафаэль Гаго
Re: метод нуждается в . Некоторые реализации коллекций предоставляют утверждение, которое вызывает утверждение. iterator cannot be decrementederaserandom access iteratorforward only iterator
Джесси Чизхольм
@Jesse Chisholm вопрос был о std :: list, а не о произвольном контейнере. std :: list обеспечивает стирание и двунаправленные итераторы.
Рафаэль Гаго
4

Я суммирую это, вот три метода с примером:

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_ifобъединения в eraseфункции-член:

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 
Jayhello
источник
2

Удаление делает недействительными только итераторы, которые указывают на удаляемые элементы.

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

Что вы можете сделать, это сначала сохранить итератор элемента, который нужно удалить, затем увеличить итератор и затем удалить сохраненный.

Ананд
источник
2
Использование постинкремента намного элегантнее.
Брайан Нил
2

Если вы думаете о std::listподобной очереди, то вы можете удалить из очереди и поставить в очередь все элементы, которые вы хотите сохранить, но только удалить из очереди (а не поставить в очередь) элемент, который вы хотите удалить. Вот пример, где я хочу удалить 5 из списка, содержащего числа 1-10 ...

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.

Алекс Багг
источник
Интересный подход, но я боюсь, что он может быть медленным, хотя.
sg7
2

Повторение в обратном направлении устраняет эффект стирания элемента на оставшихся элементах, которые необходимо пройти:

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: я не проверил тщательно, хорошо ли он стирает элементы на концах.

sancho.s ReinstateMonicaCellio
источник
Re: avoids the effect of erasing an element on the remaining elementsдля списка, вероятно, да. Для вектора, возможно, нет. Это не гарантировано для произвольных коллекций. Например, карта может решить перебалансировать себя.
Джесси Чисхолм
1

Ты можешь написать

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

std::vector::erase std::remove_ifИдиомы следует использовать , когда элементы вектора вместо списка , чтобы сохранить в макеты O (N) - или в случае , если вы пишете общий код и элементы могут быть контейнером, без эффективного способа удаления отдельных элементов (как вектор)

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;
}));
Дж. Калз
источник
-4

Я думаю, что у вас есть ошибка, я кодирую таким образом:

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) он использует дополнительный итератор, (2) приращение итератора находится в нечетном месте для цикла без веской причины для его помещения, (3) он выполняет работу канала после решения об удалении вместо до, как в ОП. Но это не неправильный ответ.
Джесси Чизхольм