Rimuovere l'elemento dal vettore, mentre nel ciclo C ++ 11 range 'for'?


97

Ho un vettore di IInventory * e sto scorrendo l'elenco usando l'intervallo C ++ 11 per, per fare cose con ciascuno.

Dopo aver eseguito alcune operazioni con uno, è possibile rimuoverlo dall'elenco ed eliminare l'oggetto. So di poter chiamare deleteil puntatore in qualsiasi momento per ripulirlo, ma qual è il modo corretto per rimuoverlo dal vettore, mentre si è nel forciclo di intervallo ? E se lo rimuovo dall'elenco il mio ciclo verrà invalidato?

std::vector<IInventory*> inv;
inv.push_back(new Foo());
inv.push_back(new Bar());

for (IInventory* index : inv)
{
    // Do some stuff
    // OK, I decided I need to remove this object from 'inv'...
}

8
Se vuoi essere fantasioso, potresti usarlo std::remove_ifcon un predicato che "fa cose" e poi restituisce true se vuoi rimuovere l'elemento.
Benjamin Lindley,

C'è un motivo per cui non puoi semplicemente aggiungere un contatore di indice e quindi usare qualcosa come inv.erase (index)?
TomJ

4
@ TomJ: Ciò rovinerebbe comunque l'iterazione.
Ben Voigt

@ TomJ e sarebbe un killer delle prestazioni: ad ogni cancellazione, puoi spostare tutti gli elementi dopo quello cancellato.
Ivan

1
@BenVoigt Ho consigliato di passare a std::listsotto
bobobobo

Risposte:


94

No, non puoi. Basato su intervallo forè per quando è necessario accedere una volta a ogni elemento di un contenitore.

Dovresti usare il forciclo normale o uno dei suoi cugini se hai bisogno di modificare il contenitore mentre procedi, accedere a un elemento più di una volta o comunque iterare in modo non lineare attraverso il contenitore.

Per esempio:

auto i = std::begin(inv);

while (i != std::end(inv)) {
    // Do some stuff
    if (blah)
        i = inv.erase(i);
    else
        ++i;
}

5
Non cancellare-rimuovere l'idioma applicabile qui?
Naveen

4
@Naveen ho deciso di non provare a farlo perché apparentemente ha bisogno di iterare su ogni elemento, fare calcoli con esso e quindi eventualmente rimuoverlo dal contenitore. Erase-remove dice che si cancellano solo gli elementi per i quali un predicato ritorna true, AFAIU, e sembra meglio in questo modo non mescolare la logica di iterazione con il predicato.
Seth Carnegie

4
@SethCarnegie Erase-remove con un lambda per il predicato lo consente elegantemente (poiché questo è già C ++ 11)
Potatoswatter

11
Non mi piace questa soluzione, è O (N ^ 2) per la maggior parte dei contenitori. remove_ifè meglio.
Ben Voigt

6
questa risposta è corretta, eraserestituisce un nuovo iteratore valido. potrebbe non essere efficiente, ma è garantito che funzioni.
sp2danny

56

Ogni volta che un elemento viene rimosso dal vettore, è necessario presumere che gli iteratori in corrispondenza o dopo l'elemento cancellato non siano più validi, poiché ciascuno degli elementi successivi all'elemento cancellato viene spostato.

Un ciclo for basato su intervalli è solo zucchero sintattico per un ciclo "normale" che utilizza gli iteratori, quindi si applica quanto sopra.

Detto questo, potresti semplicemente:

inv.erase(
    std::remove_if(
        inv.begin(),
        inv.end(),
        [](IInventory* element) -> bool {
            // Do "some stuff", then return true if element should be removed.
            return true;
        }
    ),
    inv.end()
);

5
" perché esiste la possibilità che il vettore abbia riallocato il blocco di memoria in cui conserva i suoi elementi " No, a vectornon si riallocherà mai a causa di una chiamata a erase. Il motivo per cui gli iteratori vengono invalidati è perché ciascuno degli elementi successivi all'elemento cancellato viene spostato.
ildjarn

2
Un valore predefinito per la cattura [&]sarebbe appropriato, per permettergli di "fare qualcosa" con le variabili locali.
Potatoswatter

2
Questo non sembra più semplice di un ciclo iteratore-based, in aggiunta si deve ricordare di circondare il vostro remove_ifcon .erase, altrimenti non succede nulla.
bobobobo

2
@bobobobo Se per "ciclo basato su iteratori" intendi la risposta di Seth Carnegie , questa è in media O (n ^ 2). std::remove_ifè O (n).
Branko Dimitrijevic

Hai davvero un buon punto, scambiando gli elementi in fondo alla lista ed evitando di spostare effettivamente gli elementi fino a quando tutti gli elts "da rimuovere" non sono stati completati (cosa che remove_ifdeve fare internamente). Tuttavia, se si dispone di una vectordi 5 elementi e solo tu .erase() 1 alla volta, non v'è alcun impatto sulle prestazioni per l'utilizzo di iteratori vs remove_if. Se l'elenco è più grande, dovresti davvero passare a std::listdove c'è molta rimozione di metà elenco.
bobobobo

16

Idealmente non dovresti modificare il vettore durante l'iterazione su di esso. Usa l'idioma cancella-rimuovi. In tal caso, è probabile che si verifichino alcuni problemi. Dal momento che in un vectorun eraseinvalida tutti iteratori che inizia con l'elemento venga cancellato fino alla end()dovrai fare in modo che i vostri iteratori restano valide tramite:

for (MyVector::iterator b = v.begin(); b != v.end();) { 
    if (foo) {
       b = v.erase( b ); // reseat iterator to a valid value post-erase
    else {
       ++b;
    }
}

Nota che hai bisogno del b != v.end()test così com'è. Se provi a ottimizzarlo come segue:

for (MyVector::iterator b = v.begin(), e = v.end(); b != e;)

ti imbatterai in UB poiché il tuo eviene invalidato dopo la prima erasechiamata.


@ildjarn: Sì, vero! È stato un errore di battitura.
Direttamente

2
Questo non è l'idioma cancella-rimuovi. Non c'è chiamata a std::remove, ed è O (N ^ 2) non O (N).
Potatoswatter

1
@ Potatoswatter: Certo che no. Stavo cercando di sottolineare i problemi con l'eliminazione durante l'iterazione. Immagino che la mia formulazione non abbia superato l'esame?
Direttamente

5

È un requisito rigoroso rimuovere gli elementi mentre si è in quel ciclo? Altrimenti potresti impostare i puntatori che vuoi eliminare su NULL e fare un altro passaggio sul vettore per rimuovere tutti i puntatori NULL.

std::vector<IInventory*> inv;
inv.push_back( new Foo() );
inv.push_back( new Bar() );

for ( IInventory* &index : inv )
{
    // do some stuff
    // ok I decided I need to remove this object from inv...?
    if (do_delete_index)
    {
        delete index;
        index = NULL;
    }
}
std::remove(inv.begin(), inv.end(), NULL);

1

scusa per il necropost e anche se la mia esperienza in C ++ ostacola la mia risposta, ma se stai cercando di iterare ogni elemento e apportare possibili modifiche (come cancellare un indice), prova a utilizzare un ciclo for backwords.

for(int x=vector.getsize(); x>0; x--){

//do stuff
//erase index x

}

quando si cancella l'indice x, il ciclo successivo sarà per l'elemento "davanti" all'ultima iterazione. spero davvero che questo abbia aiutato qualcuno


solo non dimenticare quando usi x per accedere a un determinato indice,
esegui

1

OK, sono in ritardo, ma comunque: scusa, non ho corretto quello che ho letto finora - è possibile, bastano due iteratori:

std::vector<IInventory*>::iterator current = inv.begin();
for (IInventory* index : inv)
{
    if(/* ... */)
    {
        delete index;
    }
    else
    {
        *current++ = index;
    }
}
inv.erase(current, inv.end());

La semplice modifica del valore a cui punta un iteratore non invalida nessun altro iteratore, quindi possiamo farlo senza doversi preoccupare. In realtà,std::remove_if (almeno l'implementazione di gcc) fa qualcosa di molto simile (usando un ciclo classico ...), semplicemente non cancella nulla e non cancella.

Tieni presente, tuttavia, che questo non è thread-safe (!) - Tuttavia, questo vale anche per alcune delle altre soluzioni sopra ...


Che diamine. Questo è eccessivo.
Kesse

@ Kesse Davvero? Questo è l'algoritmo più efficiente che puoi ottenere con i vettori (non importa se il ciclo basato su intervallo o il ciclo iteratore classico): in questo modo, sposti ogni elemento nel vettore al massimo una volta e itererai sull'intero vettore esattamente una volta. Quante volte sposteresti gli elementi successivi e itereresti sul vettore se cancellassi ogni elemento corrispondente tramite erase(a condizione di eliminare più di un singolo elemento, ovviamente)?
Aconcagua

1

Mostrerò con l'esempio, l'esempio seguente rimuove gli elementi dispari dal vettore:

void test_del_vector(){
    std::vector<int> vecInt{0, 1, 2, 3, 4, 5};

    //method 1
    for(auto it = vecInt.begin();it != vecInt.end();){
        if(*it % 2){// remove all the odds
            it = vecInt.erase(it);
        } else{
            ++it;
        }
    }

    // output all the remaining elements
    for(auto const& it:vecInt)std::cout<<it;
    std::cout<<std::endl;

    // recreate vecInt, and use method 2
    vecInt = {0, 1, 2, 3, 4, 5};
    //method 2
    for(auto it=std::begin(vecInt);it!=std::end(vecInt);){
        if (*it % 2){
            it = vecInt.erase(it);
        }else{
            ++it;
        }
    }

    // output all the remaining elements
    for(auto const& it:vecInt)std::cout<<it;
    std::cout<<std::endl;

    // recreate vecInt, and use method 3
    vecInt = {0, 1, 2, 3, 4, 5};
    //method 3
    vecInt.erase(std::remove_if(vecInt.begin(), vecInt.end(),
                 [](const int a){return a % 2;}),
                 vecInt.end());

    // output all the remaining elements
    for(auto const& it:vecInt)std::cout<<it;
    std::cout<<std::endl;

}

output aw di seguito:

024
024
024

Tieni presente che il metodo eraserestituirà l'iteratore successivo dell'iteratore passato.

Da qui , possiamo utilizzare un metodo più di generazione:

template<class Container, class F>
void erase_where(Container& c, F&& f)
{
    c.erase(std::remove_if(c.begin(), c.end(),std::forward<F>(f)),
            c.end());
}

void test_del_vector(){
    std::vector<int> vecInt{0, 1, 2, 3, 4, 5};
    //method 4
    auto is_odd = [](int x){return x % 2;};
    erase_where(vecInt, is_odd);

    // output all the remaining elements
    for(auto const& it:vecInt)std::cout<<it;
    std::cout<<std::endl;    
}

Vedi qui per vedere come usare std::remove_if. https://en.cppreference.com/w/cpp/algorithm/remove


1

In opposizione al titolo di questo thread, userei due passaggi:

#include <algorithm>
#include <vector>

std::vector<IInventory*> inv;
inv.push_back(new Foo());
inv.push_back(new Bar());

std::vector<IInventory*> toDelete;

for (IInventory* index : inv)
{
    // Do some stuff
    if (deleteConditionTrue)
    {
        toDelete.push_back(index);
    }
}

for (IInventory* index : toDelete)
{
    inv.erase(std::remove(inv.begin(), inv.end(), index), inv.end());
}

0

Una soluzione molto più elegante sarebbe passare a std::list(supponendo che non sia necessario un accesso casuale veloce).

list<Widget*> widgets ; // create and use this..

È quindi possibile eliminare con .remove_ife un funtore C ++ in una riga:

widgets.remove_if( []( Widget*w ){ return w->isExpired() ; } ) ;

Quindi qui sto solo scrivendo un funtore che accetta un argomento (il Widget*). Il valore restituito è la condizione in base alla quale rimuovere a Widget*dall'elenco.

Trovo questa sintassi appetibile. Non penso che utilizzerei mai remove_ifper std :: vectors - c'è così tanto inv.begin()e inv.end()rumore lì probabilmente è meglio usare un'eliminazione basata su indice intero o semplicemente una normale eliminazione basata su iteratore regolare (come mostrato sotto). Ma in ogni caso non dovresti rimuovere molto dal centro di un file std::vector, quindi passare a un filelist per questo caso di eliminazione frequente di metà elenco.

Nota, tuttavia, non ho avuto la possibilità di chiamare deletequelli Widget*che sono stati rimossi. Per farlo, sarebbe simile a questo:

widgets.remove_if( []( Widget*w ){
  bool exp = w->isExpired() ;
  if( exp )  delete w ;       // delete the widget if it was expired
  return exp ;                // remove from widgets list if it was expired
} ) ;

Puoi anche usare un normale ciclo basato su iteratore in questo modo:

//                                                              NO INCREMENT v
for( list<Widget*>::iterator iter = widgets.begin() ; iter != widgets.end() ; )
{
  if( (*iter)->isExpired() )
  {
    delete( *iter ) ;
    iter = widgets.erase( iter ) ; // _advances_ iter, so this loop is not infinite
  }
  else
    ++iter ;
}

Se non ti piace la lunghezza di for( list<Widget*>::iterator iter = widgets.begin() ; ..., puoi usare

for( auto iter = widgets.begin() ; ...

1
Non credo che si capisce come remove_ifin una std::vectorrealtà di lavoro, e come si mantiene la complessità O (N).
Ben Voigt

Non importa. Rimuovere dal centro di a std::vectorfarà sempre scorrere ogni elemento dopo quello che hai rimosso, rendendo una std::listscelta molto migliore.
bobobobo

1
No, non "farà scorrere ogni elemento verso l'alto di uno". remove_iffarà scorrere ogni elemento verso l'alto del numero di spazi liberati. Con il tempo si conto per l'utilizzo della cache, remove_ifin una std::vectorrimozione Sorpassa probabilmente da un std::list. E preserva O(1)l'accesso casuale.
Ben Voigt

1
Allora hai un'ottima risposta alla ricerca di una domanda. Questa domanda parla dell'iterazione dell'elenco, che è O (N) per entrambi i contenitori. E rimuovendo gli elementi O (N), che è anche O (N) per entrambi i contenitori.
Ben Voigt

2
Non è richiesta la pre-marcatura; è perfettamente possibile farlo in un unico passaggio. Devi solo tenere traccia del "prossimo elemento da ispezionare" e del "prossimo spazio da riempire". Pensalo come costruire una copia dell'elenco, filtrata in base al predicato. Se il predicato restituisce true, salta l'elemento, altrimenti copialo. Ma la copia dell'elenco viene creata e lo scambio / spostamento viene utilizzato invece di copiare.
Ben Voigt

0

Penso che farei quanto segue ...

for (auto itr = inv.begin(); itr != inv.end();)
{
   // Do some stuff
   if (OK, I decided I need to remove this object from 'inv')
      itr = inv.erase(itr);
   else
      ++itr;
}

0

non puoi eliminare l'iteratore durante l'iterazione del ciclo perché il conteggio dell'iteratore non corrisponde e dopo qualche iterazione avresti un iteratore non valido.

Soluzione: 1) prendi la copia del vettore originale 2) itera l'iteratore usando questa copia 2) fai alcune cose ed eliminalo dal vettore originale.

std::vector<IInventory*> inv;
inv.push_back(new Foo());
inv.push_back(new Bar());

std::vector<IInventory*> copyinv = inv;
iteratorCout = 0;
for (IInventory* index : copyinv)
{
    // Do some stuff
    // OK, I decided I need to remove this object from 'inv'...
    inv.erase(inv.begin() + iteratorCout);
    iteratorCout++;
}  

0

La cancellazione di un elemento uno per uno porta facilmente a prestazioni N ^ 2. Meglio contrassegnare gli elementi che dovrebbero essere cancellati e cancellarli subito dopo il ciclo. Se posso presumere nullptr in un elemento non valido nel tuo vettore, allora

std::vector<IInventory*> inv;
// ... push some elements to inv
for (IInventory*& index : inv)
{
    // Do some stuff
    // OK, I decided I need to remove this object from 'inv'...
    {
      delete index;
      index =nullptr;
    }
}
inv.erase( std::remove( begin( inv ), end( inv ), nullptr ), end( inv ) ); 

dovrebbe funzionare.

Nel caso in cui "Fai qualcosa" non stia cambiando elementi del vettore e viene utilizzato solo per prendere la decisione di rimuovere o mantenere l'elemento, puoi convertirlo in lambda (come è stato suggerito in un post precedente di qualcuno) e utilizzare

inv.erase( std::remove_if( begin( inv ), end( inv ), []( Inventory* i )
  {
    // DO some stuff
    return OK, I decided I need to remove this object from 'inv'...
  } ), end( inv ) );
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.