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.