Eliminazione di elementi da std :: set durante l'iterazione


147

Devo passare attraverso un set e rimuovere elementi che soddisfano un criterio predefinito.

Questo è il codice di test che ho scritto:

#include <set>
#include <algorithm>

void printElement(int value) {
    std::cout << value << " ";
}

int main() {
    int initNum[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    std::set<int> numbers(initNum, initNum + 10);
    // print '0 1 2 3 4 5 6 7 8 9'
    std::for_each(numbers.begin(), numbers.end(), printElement);

    std::set<int>::iterator it = numbers.begin();

    // iterate through the set and erase all even numbers
    for (; it != numbers.end(); ++it) {
        int n = *it;
        if (n % 2 == 0) {
            // wouldn't invalidate the iterator?
            numbers.erase(it);
        }
    }

    // print '1 3 5 7 9'
    std::for_each(numbers.begin(), numbers.end(), printElement);

    return 0;
}

Inizialmente, ho pensato che la cancellazione di un elemento dal set durante l'iterazione attraverso di esso avrebbe invalidato l'iteratore e l'incremento nel ciclo for avrebbe un comportamento indefinito. Anche se ho eseguito questo codice di prova e tutto è andato bene, e non riesco a spiegare il perché.

La mia domanda: è questo il comportamento definito per gli std set o questa implementazione è specifica? Sto usando gcc 4.3.3 su Ubuntu 10.04 (versione a 32 bit), comunque.

Grazie!

La soluzione proposta:

È un modo corretto di iterare e cancellare elementi dal set?

while(it != numbers.end()) {
    int n = *it;
    if (n % 2 == 0) {
        // post-increment operator returns a copy, then increment
        numbers.erase(it++);
    } else {
        // pre-increment operator increments, then return
        ++it;
    }
}

Modifica: SOLUZIONE PREFERITA

Ho trovato una soluzione che mi sembra più elegante, anche se fa esattamente lo stesso.

while(it != numbers.end()) {
    // copy the current iterator then increment it
    std::set<int>::iterator current = it++;
    int n = *current;
    if (n % 2 == 0) {
        // don't invalidate iterator it, because it is already
        // pointing to the next element
        numbers.erase(current);
    }
}

Se nel frattempo ci sono diverse condizioni di test, ognuna di esse deve incrementare l'iteratore. Mi piace di più questo codice perché l'iteratore viene incrementato solo in un punto , rendendo il codice meno soggetto a errori e più leggibile.



3
In realtà, ho letto questa domanda (e altre) prima di porre la mia, ma dal momento che erano collegati ad altri contenitori STL e dato che il mio test iniziale apparentemente funzionava, ho pensato che ci fosse una differenza tra loro. Solo dopo la risposta di Matt ho pensato di usare valgrind. Anche se preferisco la mia NUOVA soluzione rispetto alle altre perché riduce le possibilità di errori incrementando l'iteratore in un solo posto. Grazie a tutti per l'aiuto!
Pedromanoel,

1
@pedromanoel ++itdovrebbe essere un po 'più efficiente rispetto al it++fatto che non richiede l'uso di una copia temporanea invisibile dell'iteratore. La versione di Kornel, sebbene più a lungo, garantisce che gli elementi non filtrati vengano ripetuti nel modo più efficiente.
Alnitak,

@Alnitak Non ci ho pensato, ma penso che la differenza nelle prestazioni non sarebbe così grande. La copia viene creata anche nella sua versione, ma solo per gli elementi corrispondenti. Quindi il grado di ottimizzazione dipende totalmente dalla struttura dell'insieme. Per un po 'di tempo ho pre-ottimizzato il codice, danneggiando la leggibilità e la velocità di codifica nel processo ... Quindi avrei eseguito alcuni test prima di utilizzare l'altro modo.
Pedromanoel,

Risposte:


178

Questo dipende dall'implementazione:

Standard 23.1.2.8:

I membri dell'inserto non influiscono sulla validità degli iteratori e dei riferimenti al contenitore e i membri di cancellazione invalideranno solo gli iteratori e i riferimenti agli elementi cancellati.

Forse potresti provare questo - questo è conforme allo standard:

for (auto it = numbers.begin(); it != numbers.end(); ) {
    if (*it % 2 == 0) {
        numbers.erase(it++);
    }
    else {
        ++it;
    }
}

Si noti che ++ è postfix, quindi passa la vecchia posizione per cancellare, ma prima passa a una nuova a causa dell'operatore.

Aggiornamento 2015.10.27: C ++ 11 ha risolto il difetto. iterator erase (const_iterator position);restituisce un iteratore all'elemento che segue l'ultimo elemento rimosso (o set::end, se l'ultimo elemento è stato rimosso). Quindi lo stile C ++ 11 è:

for (auto it = numbers.begin(); it != numbers.end(); ) {
    if (*it % 2 == 0) {
        it = numbers.erase(it);
    }
    else {
        ++it;
    }
}

2
Questo non funziona con deque MSVC2013. O la loro implementazione è errata o c'è ancora un altro requisito che impedisce a questo di funzionare deque. Le specifiche STL sono così complicate che non puoi aspettarti che tutte le implementazioni le seguano, e tanto meno il tuo programmatore occasionale lo memorizzi. STL è un mostro oltre l'addomesticamento e poiché non esiste un'implementazione unica (e le suite di test, se ce ne sono, apparentemente non coprono casi ovvi come l'eliminazione di elementi in un ciclo), ciò rende l'STL un giocattolo fragile e brillante che può andare avanti con un botto quando lo guardi di lato.
Kuroi Neko,

@MatthieuM. Lo fa in C ++ 11. In C ++ 17, ora richiede iterator (const_iterator in C ++ 11).
tartaruga_casco_mole

19

Se esegui il tuo programma attraverso valgrind, vedrai un sacco di errori di lettura. In altre parole, sì, gli iteratori vengono invalidati, ma sei fortunato nel tuo esempio (o davvero sfortunato, poiché non vedi gli effetti negativi di un comportamento indefinito). Una soluzione a questo è quella di creare un iteratore temporaneo, aumentare la temperatura, eliminare l'iteratore di destinazione, quindi impostare la destinazione su temp. Ad esempio, riscrivi il tuo ciclo come segue:

std::set<int>::iterator it = numbers.begin();                               
std::set<int>::iterator tmp;                                                

// iterate through the set and erase all even numbers                       
for ( ; it != numbers.end(); )                                              
{                                                                           
    int n = *it;                                                            
    if (n % 2 == 0)                                                         
    {                                                                       
        tmp = it;                                                           
        ++tmp;                                                              
        numbers.erase(it);                                                  
        it = tmp;                                                           
    }                                                                       
    else                                                                    
    {                                                                       
        ++it;                                                               
    }                                                                       
} 

Se è solo la condizione che conta e non richiede l'inizializzazione o post-operazione nell'ambito, allora è meglio usare il whileciclo. cioè for ( ; it != numbers.end(); )è meglio visibile conwhile (it != numbers.end())
iammilind il

7

Si fraintende il significato di "comportamento indefinito". Un comportamento indefinito non significa "se lo fai, il tuo programma si arresta in modo anomalo o produce risultati imprevisti". Significa "se lo fai, il tuo programma potrebbe andare in crash o produrre risultati imprevisti" o fare qualsiasi altra cosa, a seconda del tuo compilatore, del tuo sistema operativo, della fase della luna, ecc.

Se qualcosa viene eseguito senza arresti anomali e si comporta come previsto, questa non è la prova che non si tratta di un comportamento indefinito. Tutto ciò che dimostra è che il suo comportamento è risultato essere quello osservato per quella particolare esecuzione dopo la compilazione con quel particolare compilatore su quel particolare sistema operativo.

La cancellazione di un elemento da un set invalida l'iteratore all'elemento cancellato. L'uso di un iteratore non valido è un comportamento indefinito. È successo che il comportamento osservato fosse quello che intendevi in ​​questo caso particolare; non significa che il codice sia corretto.


Oh, sono ben consapevole che un comportamento indefinito può anche significare "Funziona per me, ma non per tutti". Ecco perché ho posto questa domanda, perché non sapevo se questo comportamento fosse corretto o meno. Se lo fosse, me ne andrei via così. Usare un ciclo while risolverebbe il mio problema, allora? Ho modificato la mia domanda con la soluzione proposta. Per favore controlla.
pedromanoel,

Funziona anche per me. Ma quando cambio la condizione in if (n > 2 && n < 7 )allora ottengo 0 1 2 4 7 8 9. - Il risultato particolare qui probabilmente dipende più dai dettagli di implementazione del metodo di cancellazione e imposta iteratori, piuttosto che dalla fase della luna (non quella dovrebbe mai fare affidamento sui dettagli di implementazione). ;)
UncleBens,

1
STL aggiunge molti nuovi significati al "comportamento indefinito". Ad esempio "Microsoft ha pensato in modo intelligente a migliorare le specifiche consentendo std::set::erasedi restituire un iteratore, quindi il tuo codice MSVC si riempirà di botto quando compilato da gcc", o "Microsoft esegue controlli associati in std::bitset::operator[]modo che il tuo algoritmo bitset accuratamente ottimizzato rallenti a un ricerca per indicizzazione quando compilato con MSVC ". STL non ha un'implementazione unica e le sue specifiche sono un disastro gonfiato in crescita esponenziale, quindi non c'è da meravigliarsi che eliminare elementi da un ciclo richieda esperienza da programmatore senior ...
kuroi neko

2

Giusto per avvertire che, nel caso di un contenitore di deque, tutte le soluzioni che controllano l'eguaglianza di deque iteratore rispetto a numbers.end () probabilmente falliranno su gcc 4.8.4. Vale a dire, la cancellazione di un elemento del deque generalmente invalida il puntatore a numbers.end ():

#include <iostream>
#include <deque>

using namespace std;
int main() 
{

  deque<int> numbers;

  numbers.push_back(0);
  numbers.push_back(1);
  numbers.push_back(2);
  numbers.push_back(3);
  //numbers.push_back(4);

  deque<int>::iterator  it_end = numbers.end();

  for (deque<int>::iterator it = numbers.begin(); it != numbers.end(); ) {
    if (*it % 2 == 0) {
      cout << "Erasing element: " << *it << "\n";
      numbers.erase(it++);
      if (it_end == numbers.end()) {
    cout << "it_end is still pointing to numbers.end()\n";
      } else {
    cout << "it_end is not anymore pointing to numbers.end()\n";
      }
    }
    else {
      cout << "Skipping element: " << *it << "\n";
      ++it;
    }
  }
}

Produzione:

Erasing element: 0
it_end is still pointing to numbers.end()
Skipping element: 1
Erasing element: 2
it_end is not anymore pointing to numbers.end()

Si noti che mentre la trasformazione del deque è corretta in questo caso particolare, il puntatore di fine è stato invalidato lungo la strada. Con il deque di una dimensione diversa l'errore è più evidente:

int main() 
{

  deque<int> numbers;

  numbers.push_back(0);
  numbers.push_back(1);
  numbers.push_back(2);
  numbers.push_back(3);
  numbers.push_back(4);

  deque<int>::iterator  it_end = numbers.end();

  for (deque<int>::iterator it = numbers.begin(); it != numbers.end(); ) {
    if (*it % 2 == 0) {
      cout << "Erasing element: " << *it << "\n";
      numbers.erase(it++);
      if (it_end == numbers.end()) {
    cout << "it_end is still pointing to numbers.end()\n";
      } else {
    cout << "it_end is not anymore pointing to numbers.end()\n";
      }
    }
    else {
      cout << "Skipping element: " << *it << "\n";
      ++it;
    }
  }
}

Produzione:

Erasing element: 0
it_end is still pointing to numbers.end()
Skipping element: 1
Erasing element: 2
it_end is still pointing to numbers.end()
Skipping element: 3
Erasing element: 4
it_end is not anymore pointing to numbers.end()
Erasing element: 0
it_end is not anymore pointing to numbers.end()
Erasing element: 0
it_end is not anymore pointing to numbers.end()
...
Segmentation fault (core dumped)

Ecco uno dei modi per risolvere questo problema:

#include <iostream>
#include <deque>

using namespace std;
int main() 
{

  deque<int> numbers;
  bool done_iterating = false;

  numbers.push_back(0);
  numbers.push_back(1);
  numbers.push_back(2);
  numbers.push_back(3);
  numbers.push_back(4);

  if (!numbers.empty()) {
    deque<int>::iterator it = numbers.begin();
    while (!done_iterating) {
      if (it + 1 == numbers.end()) {
    done_iterating = true;
      } 
      if (*it % 2 == 0) {
    cout << "Erasing element: " << *it << "\n";
      numbers.erase(it++);
      }
      else {
    cout << "Skipping element: " << *it << "\n";
    ++it;
      }
    }
  }
}

L'essere chiave do not trust an old remembered dq.end() value, always compare to a new call to dq.end().
Jesse Chisholm,

2

C ++ 20 avrà la "cancellazione uniforme del contenitore" e sarai in grado di scrivere:

std::erase_if(numbers, [](int n){ return n % 2 == 0 });

E che lavorerà per vector, set, deque, ecc Vedere cppReference per maggiori informazioni.


1

Questo comportamento è specifico dell'implementazione. Per garantire la correttezza dell'iteratore dovresti usare "it = numbers.erase (it);" dichiarazione se è necessario eliminare l'elemento e semplicemente ripetere l'iteratore in altri casi.


1
Set<T>::erasela versione non restituisce iteratore.
Arkaitz Jimenez,

4
In realtà lo fa, ma solo sull'implementazione di MSVC. Quindi questa è veramente una risposta specifica per l'implementazione. :)
Eugene,

1
@Eugene Lo fa per tutte le implementazioni con C ++ 11
mastov

Alcune implementazioni di gcc 4.8with c++1yhanno un bug in fase di cancellazione. it = collection.erase(it);dovrebbe funzionare, ma potrebbe essere più sicuro da usarecollection.erase(it++);
Jesse Chisholm,

1

Penso che l'utilizzo del metodo STL " remove_if" da possa aiutare a prevenire alcuni strani problemi quando si tenta di eliminare l'oggetto racchiuso dall'iteratore.

Questa soluzione potrebbe essere meno efficiente.

Diciamo che abbiamo un qualche tipo di contenitore, come vector o un elenco chiamato m_bullets:

Bullet::Ptr is a shared_pr<Bullet>

' it' è l'iteratore che ' remove_if' restituisce, il terzo argomento è una funzione lambda che viene eseguita su ogni elemento del contenitore. Poiché il contenitore contiene Bullet::Ptr, la funzione lambda deve ottenere quel tipo (o un riferimento a quel tipo) passato come argomento.

 auto it = std::remove_if(m_bullets.begin(), m_bullets.end(), [](Bullet::Ptr bullet){
    // dead bullets need to be removed from the container
    if (!bullet->isAlive()) {
        // lambda function returns true, thus this element is 'removed'
        return true;
    }
    else{
        // in the other case, that the bullet is still alive and we can do
        // stuff with it, like rendering and what not.
        bullet->render(); // while checking, we do render work at the same time
        // then we could either do another check or directly say that we don't
        // want the bullet to be removed.
        return false;
    }
});
// The interesting part is, that all of those objects were not really
// completely removed, as the space of the deleted objects does still 
// exist and needs to be removed if you do not want to manually fill it later 
// on with any other objects.
// erase dead bullets
m_bullets.erase(it, m_bullets.end());

' remove_if' rimuove il contenitore in cui la funzione lambda ha restituito true e sposta tale contenuto all'inizio del contenitore. ' it' Indica un oggetto indefinito che può essere considerato spazzatura. Gli oggetti da 'it' a m_bullets.end () possono essere cancellati, poiché occupano memoria, ma contengono spazzatura, quindi il metodo 'cancella' viene chiamato su quell'intervallo.


0

Mi sono imbattuto nello stesso vecchio problema e ho trovato il codice seguente più comprensibile, il che è in un certo senso per le soluzioni di cui sopra.

std::set<int*>::iterator beginIt = listOfInts.begin();
while(beginIt != listOfInts.end())
{
    // Use your member
    std::cout<<(*beginIt)<<std::endl;

    // delete the object
    delete (*beginIt);

    // erase item from vector
    listOfInts.erase(beginIt );

    // re-calculate the begin
    beginIt = listOfInts.begin();
}

Funziona solo se cancellerai sempre ogni elemento. L'OP consiste nel cancellare selettivamente gli elementi e avere ancora iteratori validi.
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.