Puoi rimuovere elementi da un elenco std :: mentre lo stai ripetendo?


239

Ho un codice simile a questo:

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

Vorrei rimuovere gli elementi inattivi immediatamente dopo averli aggiornati, in ordine per evitare di ripetere l'elenco. Ma se aggiungo le righe commentate, ricevo un errore quando arrivo a i++: "Elenco iteratore non incrementabile". Ho provato alcuni sostituti che non sono aumentati nell'istruzione for, ma non sono riuscito a far funzionare nulla.

Qual è il modo migliore per rimuovere elementi mentre stai camminando su una lista std ::?


Non ho visto alcuna soluzione basata sull'iterazione all'indietro. Ne ho pubblicato uno .
— sancho.s RipristinaMonicaCellio il

Risposte:


286

Devi prima incrementare l'iteratore (con i ++) e quindi rimuovere l'elemento precedente (ad es. Usando il valore restituito da i ++). Puoi cambiare il codice in un ciclo while in questo modo:

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
In realtà, non è garantito che funzioni. Con "erase (i ++);", sappiamo solo che il valore pre-incrementato viene passato a erase () e che i viene incrementato prima del punto e virgola, non necessariamente prima della chiamata a erase (). "iteratore prev = i ++; erase (prev);" funzionerà sicuramente, così come usa il valore di ritorno
— James Curran,

58
No James, i viene incrementato prima di chiamare cancella e il valore precedente viene passato alla funzione. Gli argomenti di una funzione devono essere completamente valutati prima che la funzione venga chiamata.
— Brian Neal,

28
@ James Curran: non è corretto. TUTTI gli argomenti vengono completamente valutati prima che venga chiamata una funzione.
— Martin York,

9
Martin York ha ragione. Tutti gli argomenti di una chiamata di funzione vengono completamente valutati prima che venga chiamata una funzione, senza eccezioni. Questo è semplicemente come funziona la funzione. E non ha nulla a che fare con il tuo esempio foo.b (i ++). C (i ++) (che non è definito in nessun caso)
— jalf,

75
L'uso alternativo i = items.erase(i)è più sicuro, perché equivale a un elenco, ma funzionerà comunque se qualcuno cambia il contenitore in un vettore. Con un vettore, cancella () sposta tutto a sinistra per riempire il buco. Se si tenta di rimuovere l'ultimo elemento con il codice che incrementa l'iteratore dopo la cancellazione, la fine si sposta a sinistra e l'iteratore si sposta a destra, oltre la fine. E poi ti schianti.
— Eric Seppanen,

133

Vuoi fare:

i= items.erase(i);

Ciò aggiornerà correttamente l'iteratore in modo che punti alla posizione dopo l'iteratore che hai rimosso.


80
Tieni presente che non puoi semplicemente inserire quel codice nel tuo for-loop. Altrimenti salterai un elemento ogni volta che ne rimuoverai uno.
— Michael Kristofik,

2
non può farlo io--; ogni volta che segue il suo pezzo di codice per evitare di saltare?
— enthusiasticgeek

1
@enthusiasticgeek, cosa succede se i==items.begin()?
— MSN

1
@enthusiasticgeek, a quel punto dovresti semplicemente farlo i= items.erase(i);. È la forma canonica e si occupa già di tutti quei dettagli.
— MSN

6
Michael sottolinea un enorme 'gotcha', dovevo affrontare la stessa cosa proprio ora. Il metodo più semplice per evitarlo che ho scoperto è stato semplicemente decomporre il ciclo for () in un po '() e fare attenzione all'incremento
— Anne Quinn,

22

Devi fare la combinazione della risposta di Kristo e di 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);
    }
}

Naturalmente, la cosa più efficiente e superCool® STL sarebbe qualcosa del genere:

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

Ho preso in considerazione il tuo metodo SuperCool, la mia esitazione è stata che la chiamata a remove_if non chiarisce che l'obiettivo è quello di elaborare gli elementi, piuttosto che rimuoverli dall'elenco di quelli attivi. (Gli elementi non vengono eliminati perché diventano solo inattivi, non necessari)
— AShelly,

Suppongo tu abbia ragione. Da un lato sono propenso a suggerire di cambiare il nome di "aggiornamento" per rimuovere l'oscurità, ma la verità è che questo codice è stravagante con i funzioni, ma è anche tutt'altro che oscuro.
— Mike

Commento corretto, correggi il ciclo while per utilizzare end o elimina la definizione non utilizzata.
— Mike,

10

Usa l'algoritmo std :: remove_if.

Modifica: lavorare con le raccolte dovrebbe essere come: 1. preparare la raccolta. 2. raccolta dei processi.

La vita sarà più facile se non mescolerai questi passaggi.

  1. std :: remove_if. or list :: remove_if (se sai che lavori con list e non con TCollection)
  2. std :: for_each

2
std :: list ha una funzione membro remove_if che è più efficiente dell'algoritmo remove_if (e non richiede il linguaggio "remove-erase").
— Brian Neal,

5

Ecco un esempio che utilizza un forciclo che scorre l'elenco e incrementa o riconvalida l'iteratore nel caso in cui un elemento venga rimosso durante l'attraversamento dell'elenco.

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

L'alternativa per la versione loop alla risposta di Kristo.

Perdi un po 'di efficienza, vai indietro e poi di nuovo in avanti durante l'eliminazione, ma in cambio dell'incremento dell'iteratore aggiuntivo puoi far dichiarare l'iteratore nell'ambito del ciclo e il codice apparire un po' più pulito. Cosa scegliere dipende dalle priorità del momento.

La risposta era totalmente fuori dal tempo, lo so ...

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
È quello che ho usato anche io. Ma non sono sicuro che funzioni se l'elemento da rimuovere è il primo elemento nel contenitore. Per me funziona, penso, ma non sono sicuro che sia portatile su più piattaforme.
— trololo,

Non ho fatto "-1", tuttavia, l'elenco iteratore non può diminuire? Almeno ho un'affermazione da Visual Studio 2008.
— Miglia il

Fintanto che l'elenco collegato è implementato come un elenco doppio collegamento circolare con un nodo head / stub (usato come end () rbegin () e quando vuoto usato anche come begin () e rend ()) funzionerà. Non ricordo in quale piattaforma stavo usando questo, ma funzionava anche per me, poiché l'implementazione sopra menzionata è l'implementazione più comune per un elenco std ::. Comunque, è quasi certo che questo stava sfruttando un comportamento indefinito (secondo lo standard C ++), quindi meglio non usarlo.
— Rafael Gago,

ri: iterator cannot be decrementedIl erasemetodo ha bisogno di a random access iterator. Alcune implementazioni di raccolta forniscono un forward only iteratorelemento che provoca l'asserzione.
— Jesse Chisholm,

@Jesse Chisholm la domanda riguardava una lista std ::, non un contenitore arbitrario. std :: list fornisce iteratori di cancellazione e bidirezionali.
— Rafael Gago,

4

Ho riassunto, ecco i tre metodi con l'esempio:

1. utilizzo di whileloop

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. utilizzo della remove_iffunzione membro nell'elenco:

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. utilizzo della std::remove_iffunzione combinata con la erasefunzione membro:

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. usando forloop, dovresti notare aggiornare l'iteratore:

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

La rimozione invalida solo gli iteratori che puntano agli elementi rimossi.

Quindi, in questo caso, dopo aver rimosso * i, sono invalidato e non è possibile incrementarlo.

Quello che puoi fare è prima salvare l'iteratore dell'elemento che deve essere rimosso, quindi incrementare l'iteratore e quindi rimuovere quello salvato.


2
L'uso di post-incremento è molto più elegante.
— Brian Neal,

2

Se pensi std::lista una coda simile, puoi dequeue e accodare tutti gli oggetti che vuoi conservare, ma solo dequeue (e non accodare) l'elemento che vuoi rimuovere. Ecco un esempio in cui voglio rimuovere 5 da un elenco contenente i numeri 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 ora avrà solo i numeri 1-4 e 6-10.


Approccio interessante ma temo che potrebbe essere lento.
— sg7,

2

L'iterazione all'indietro evita l'effetto di cancellare un elemento sugli elementi rimanenti da attraversare:

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: vedi questo , ad esempio, riguardo all'iterazione all'indietro.

PS2: non ho testato a fondo se gestisce bene la cancellazione di elementi alle estremità.


ri: avoids the effect of erasing an element on the remaining elementsper un elenco, probabilmente sì. Per un vettore forse no. Questo non è garantito nelle raccolte arbitrarie. Ad esempio, una mappa può decidere di riequilibrarsi.
— Jesse Chisholm,

1

Tu puoi scrivere

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

Puoi scrivere un codice equivalente con std::list::remove_if, che è meno dettagliato e più esplicito

items.remove_if([] (item*i) {
    bool isActive = (*i)->update();
    if (!isActive) 
        return true;

    other_code_involving(*i);
    return false;
});

Il std::vector::erase std::remove_iflinguaggio dovrebbe essere usato quando gli elementi sono un vettore anziché un elenco per mantenere la complicità su O (n) - o nel caso in cui si scriva un codice generico e gli elementi potrebbero essere un contenitore senza un modo efficace per cancellare singoli elementi (come un vettore)

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

Penso che tu abbia un bug lì, codifico in questo modo:

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

Immagino che questo sia stato sottoposto a downgrade per preferenze di stile, in quanto sembra essere funzionale. Sì, certo, (1) usa un iteratore extra, (2) l'incremento dell'iteratore è in una posizione dispari per un loop senza una buona ragione per metterlo lì, (3) fa funzionare il canale dopo la decisione di eliminare invece di prima come nell'OP. Ma non è una risposta sbagliata.
— Jesse Chisholm,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.