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

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


Я не видел никакого решения, основанного на повторении в обратном направлении. Я выложил один такой .
— 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);

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


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 

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


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) он выполняет работу канала после решения об удалении вместо до, как в ОП. Но это не неправильный ответ.
— Джесси Чизхольм
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.