Чи можете ви видалити елементи зі списку std :: під час ітерації через нього?


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++: "Список ітераторів не збільшується". Я спробував кілька заступників, які не збільшувались у заяві для заяви, але я не міг нічого працювати.

Який найкращий спосіб видалити елементи під час перегляду списку std ::?


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

Відповіді:


286

Ви повинні спочатку наростити ітератор (з i ++), а потім видалити попередній елемент (наприклад, використовуючи повернене значення з i ++). Ви можете змінити код на циклічний час так:

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 ++);" ми знаємо лише, що попередньо збільшене значення передається для стерти (), а i збільшується перед напівкрапкою, не обов'язково перед викликом на видалення (). "ітератор prev = i ++; стерти (prev);" обов'язково спрацює, як і використовувати зворотну величину
Джеймс Курран

58
Ні, Джеймс, я збільшується перед викликом стирання, і попереднє значення передається функції. Аргументи функції повинні бути повністю оцінені до виклику функції.
Брайан Ніл

28
@ Джеймс Курран: Це не правильно. ВСІ аргументи повністю оцінюються перед викликом функції.
Мартін Йорк

9
Мартін Йорк прав. Усі аргументи виклику функції повністю оцінюються до виклику функції, без винятку. Ось так просто функціонує. І це не має нічого спільного з вашим прикладом foo.b (i ++). C (i ++) приклад (який у жодному разі не визначено)
jalf

75
Альтернативне використання i = items.erase(i)більш безпечне, оскільки воно еквівалентне для списку, але все одно працюватиме, якщо хтось змінить контейнер на вектор. За допомогою вектора стирання () переміщує все ліворуч, щоб заповнити отвір. Якщо ви спробуєте видалити останній елемент з кодом, який збільшує ітератор після стирання, кінець переміщується вліво, а ітератор рухається вправо - повз кінець. І тоді ти вріжешся.
Ерік Сеппанен

133

Ви хочете зробити:

i= items.erase(i);

Це правильно оновить ітератор, щоб вказати на місце після видаленого ітератора.


80
Попереджуйте, що ви не можете просто скинути цей код у свою for-loop. Інакше ви будете пропускати елемент кожного разу, коли ви видаляєте його.
Майкл Кристофік

2
чи може він цього не робити; щоразу слідуючи його коду, щоб уникнути пропуску?
Энтузиазмек

1
@enthusiasticgeek, що станеться, якщо i==items.begin()?
MSN

1
@enthusiasticgeek, в цей момент ви просто повинні зробити i= items.erase(i);. Це канонічна форма і піклується про всі ці деталі вже.
MSN

6
Майкл вказує на величезну "готчу", я мав справу з тим же самим зараз. Найпростіший спосіб уникнути цього, який я знайшов, - це просто розкладати цикл for () на деякий час () і бути обережним із збільшенням
Енн Квінн

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, моє вагання полягало в тому, що заклик до видалення_if не дає зрозуміти, що мета - обробити елементи, а не видалити їх зі списку активних. (Елементи не видаляються, тому що вони стають лише неактивними, не
зайвими

Я гадаю, ти маєш рацію. З одного боку, я схильний запропонувати змінити назву "update", щоб усунути невідомість, але правда, цей код є фантазією функторів, але це також все, крім неясного.
Майк

Справедливий коментар: або виправте цикл while, щоб використовувати кінець, або видаліть невикористане визначення.
Майк

10

Використовуйте алгоритм std :: remove_if.

Редагувати: робота з колекціями повинна бути такою: 1. підготувати колекцію. 2. збір процесу.

Життя буде простішим, якщо ви не змішите ці кроки.

  1. std :: remove_if. або список :: remove_if (якщо ви знаєте, що ви працюєте зі списком, а не з TCollection)
  2. std :: for_each

2
std :: list має функцію-члена delete_if, яка є більш ефективною, ніж алгоритм Remove_if (і не вимагає ідіоми "видалити-стерти").
Брайан Ніл

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
Це я теж використав. Але я не впевнений, чи гарантовано це буде працювати, якщо елемент, який потрібно видалити, є першим елементом у контейнері. Для мене це працює, я думаю, але я не впевнений, чи він портативний на різних платформах.
trololo

Я не робив "-1", проте ітератор списку не може декрементувати? Принаймні, я отримав твердження від Visual Studio 2008.
milesma

Поки зв'язаний список реалізований у вигляді кругового подвійного зв'язаного списку, що має вузол head / stub (використовується як end () rbegin () і коли порожній використовується як start () і rend () також), це буде працювати. Я не можу згадати, на якій платформі я цим користувався, але він працював і для мене, оскільки названа вище реалізація є найпоширенішою реалізацією для std :: list. Але в будь-якому випадку, майже впевнений, що це використовувало деяку не визначену (за стандартом C ++) поведінку, тому краще не користуйтеся цим.
Рафаель Гаго

Re: метод потребує . Деякі реалізації колекцій забезпечують те, що викликає твердження. iterator cannot be decrementederaserandom access iteratorforward only iterator
Jesse Chisholm

@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) він виконує роботу каналу після прийняття рішення про видалення раніше, як в ОП. Але це неправильна відповідь.
Jesse Chisholm
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.