Element aus dem Vektor entfernen, während in C ++ 11 Bereich 'für' Schleife?

96

Ich habe einen Vektor von IInventory * und durchlaufe die Liste mit dem C ++ 11-Bereich für, um mit jedem etwas zu tun.

Nachdem ich einige Dinge mit einem gemacht habe, möchte ich es vielleicht aus der Liste entfernen und das Objekt löschen. Ich weiß, dass ich deleteden Zeiger jederzeit aufrufen kann, um ihn zu bereinigen, aber wie kann ich ihn in der Bereichsschleife ordnungsgemäß aus dem Vektor entfernen for? Und wenn ich es aus der Liste entferne, wird meine Schleife ungültig?

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'...
}
EddieV223
quelle
8
Wenn Sie Lust haben, können Sie std::remove_ifein Prädikat verwenden, das "Sachen macht" und dann true zurückgibt, wenn Sie das Element entfernen möchten.
Benjamin Lindley
Gibt es einen Grund, warum Sie nicht einfach einen Indexzähler hinzufügen und dann so etwas wie inv.erase (index) verwenden können?
TomJ
4
@ TomJ: Das würde die Iteration immer noch vermasseln.
Ben Voigt
@TomJ und es wäre ein Leistungskiller - bei jedem Löschen können Sie alle Elemente nach dem gelöschten verschieben.
Ivan
1
@ BenVoigt Ich empfahl, zu std::listunten zu
wechseln

Antworten:

91

Nein, das kannst du nicht. Bereichsbasiert forist, wenn Sie einmal auf jedes Element eines Containers zugreifen müssen.

Sie sollten die normale forSchleife oder einen ihrer Cousins ​​verwenden, wenn Sie den Container im Laufe der Zeit ändern, mehrmals auf ein Element zugreifen oder auf andere Weise nichtlinear durch den Container iterieren müssen.

Beispielsweise:

auto i = std::begin(inv);

while (i != std::end(inv)) {
    // Do some stuff
    if (blah)
        i = inv.erase(i);
    else
        ++i;
}
Seth Carnegie
quelle
5
Würde hier nicht das Lösch-Entfernen-Idiom zutreffen?
Naveen
4
@Naveen Ich habe beschlossen, das nicht zu versuchen, weil er anscheinend jedes Element durchlaufen, Berechnungen damit durchführen und es dann möglicherweise aus dem Container entfernen muss. Löschen-Entfernen besagt, dass Sie nur Elemente löschen, für die ein Prädikat trueAFAIU zurückgibt, und es auf diese Weise besser erscheint, die Iterationslogik nicht mit dem Prädikat zu mischen.
Seth Carnegie
4
@ SethCarnegie Löschen-Entfernen mit einem Lambda für das Prädikat ermöglicht elegant, dass (da dies bereits C ++ 11 ist)
Potatoswatter
11
Diese Lösung gefällt mir nicht, sie ist für die meisten Container O (N ^ 2). remove_ifist besser.
Ben Voigt
6
Diese Antwort ist korrekt und erasegibt einen neuen gültigen Iterator zurück. Es ist vielleicht nicht effizient, aber es funktioniert garantiert.
SP2danny
56

Jedes Mal, wenn ein Element aus dem Vektor entfernt wird, müssen Sie davon ausgehen, dass die Iteratoren am oder nach dem gelöschten Element nicht mehr gültig sind, da jedes der Elemente, die auf das gelöschte Element folgen, verschoben wird.

Eine bereichsbasierte for-Schleife ist nur syntaktischer Zucker für eine "normale" Schleife, die Iteratoren verwendet. Daher gilt das Obige.

Davon abgesehen könnten Sie einfach:

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()
);
Branko Dimitrijevic
quelle
5
" weil es die Möglichkeit gibt, dass der Vektor den Speicherblock, in dem er seine Elemente aufbewahrt, neu zugewiesen hat " Nein, a vectorwird aufgrund eines Aufrufs von niemals neu zugewiesen erase. Der Grund, warum die Iteratoren ungültig werden, liegt darin, dass jedes der Elemente, die auf das gelöschte Element folgen, verschoben wird.
ildjarn
2
Ein Capture-Standard von [&]wäre angemessen, damit er mit lokalen Variablen "ein paar Sachen machen" kann.
Potatoswatter
2
Das sieht nicht einfacher als Iterator-basierte Schleife, zusätzlich müssen Sie daran denken , Ihre umgeben remove_ifmit .erase, sonst passiert nichts.
Bobobobo
2
@bobobobo Wenn mit "iteratorbasierter Schleife" die Antwort von Seth Carnegie gemeint ist, ist dies im Durchschnitt O (n ^ 2). std::remove_ifist O (n).
Branko Dimitrijevic
Sie haben einen wirklich guten Punkt, indem Sie Elemente in den hinteren Teil der Liste tauschen und vermeiden, dass sich Elemente tatsächlich bewegen , bis alle "zu entfernenden" Elemente fertig sind (was remove_ifintern erfolgen muss). Wenn Sie jedoch eine haben vectoraus 5 Elementen und Sie nur .erase() 1 zu einer Zeit, gibt es keine Auswirkungen auf die Leistung für die Verwendung von Iteratoren vs remove_if. Wenn die Liste größer ist, sollten Sie wirklich zu einem Ort wechseln, std::listan dem viele Listen in der Mitte der Liste entfernt werden.
Bobobobo
16

Idealerweise sollten Sie den Vektor nicht ändern, während Sie darüber iterieren. Verwenden Sie die Lösch-Entfernungs-Sprache. Wenn Sie dies tun, werden Sie wahrscheinlich auf einige Probleme stoßen. Da in a vectoran erasealle Iteratoren ungültig werden, beginnend mit dem zu löschenden Element bis zum, müssen end()Sie sicherstellen, dass Ihre Iteratoren gültig bleiben, indem Sie Folgendes verwenden:

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

Beachten Sie, dass Sie den b != v.end()Test unverändert benötigen . Wenn Sie versuchen, es wie folgt zu optimieren:

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

Sie werden auf UB stoßen, da Ihre enach dem ersten eraseAnruf ungültig ist .

dirkgently
quelle
@ildjarn: Ja, stimmt! Das war ein Tippfehler.
dirkgently
1
Dies ist nicht die Redewendung zum Löschen und Entfernen. Es gibt keinen Anruf std::removeund es ist O (N ^ 2), nicht O (N).
Potatoswatter
1
@ Potatoswatter: Natürlich nicht. Ich habe versucht, auf die Probleme beim Löschen hinzuweisen, während Sie iterieren. Ich denke, meine Formulierung hat die Musterung nicht bestanden?
dirkgently
5

Ist es eine strikte Anforderung, Elemente in dieser Schleife zu entfernen? Andernfalls können Sie die zu löschenden Zeiger auf NULL setzen und den Vektor erneut durchlaufen, um alle NULL-Zeiger zu entfernen.

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);
Yexo
quelle
1

Es tut mir leid für das Nekroposting und auch, wenn meine C ++ - Expertise meiner Antwort im Wege steht. Wenn Sie jedoch versuchen, jedes Element zu durchlaufen und mögliche Änderungen vorzunehmen (z. B. das Löschen eines Index), versuchen Sie, eine Backwords for-Schleife zu verwenden.

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

//do stuff
//erase index x

}

Beim Löschen des Index x befindet sich die nächste Schleife für das Element "vor" der letzten Iteration. Ich hoffe wirklich, dass dies jemandem geholfen hat

lilbigwill99
quelle
Vergessen Sie nicht, wenn Sie x verwenden, um auf einen bestimmten Index zuzugreifen.
Machen
1

OK, ich bin spät dran, aber trotzdem: Entschuldigung, nicht richtig, was ich bisher gelesen habe - es ist möglich, dass Sie nur zwei Iteratoren benötigen:

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

Nur das Ändern des Werts, auf den ein Iterator zeigt, macht keinen anderen Iterator ungültig, sodass wir dies tun können, ohne uns Sorgen machen zu müssen. Tatsächlich macht std::remove_if(zumindest die gcc-Implementierung) etwas sehr Ähnliches (unter Verwendung einer klassischen Schleife ...), löscht einfach nichts und löscht nicht.

Beachten Sie jedoch, dass dies nicht threadsicher (!) Ist - dies gilt jedoch auch für einige der anderen oben genannten Lösungen ...

Aconcagua
quelle
Was zum Teufel. Das ist ein Overkill.
Kesse
@ Kesse Wirklich? Dies ist der effizienteste Algorithmus, den Sie mit Vektoren erhalten können (egal ob bereichsbasierte Schleife oder klassische Iteratorschleife): Auf diese Weise verschieben Sie jedes Element im Vektor höchstens einmal und iterieren genau einmal über den gesamten Vektor. Wie oft würden Sie nachfolgende Elemente verschieben und über den Vektor iterieren, wenn Sie jedes übereinstimmende Element über löschen würdenerase löschen würden (vorausgesetzt, Sie löschen natürlich mehr als ein einzelnes Element)?
Aconcagua
1

Ich werde anhand eines Beispiels zeigen, wie das folgende Beispiel ungerade Elemente aus dem Vektor entfernt:

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;

}

Ausgabe aw unten:

024
024
024

Beachten Sie, dass die Methode eraseden nächsten Iterator des übergebenen Iterators zurückgibt.

Von hier aus können wir eine generiertere Methode verwenden:

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

Sehen Sie hier, um zu sehen, wie man es benutzt std::remove_if. https://en.cppreference.com/w/cpp/algorithm/remove

Jayhello
quelle
1

Im Gegensatz zu diesem Thread-Titel würde ich zwei Durchgänge verwenden:

#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());
}
nikc
quelle
0

Eine viel elegantere Lösung wäre der Wechsel zu std::list(vorausgesetzt, Sie benötigen keinen schnellen Direktzugriff).

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

Sie können dann mit .remove_ifund einem C ++ - Funktor in einer Zeile löschen :

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

Also schreibe ich hier nur einen Funktor, der ein Argument (das Widget*) akzeptiert . Der Rückgabewert ist die Bedingung, unter der a Widget*aus der Liste entfernt werden soll.

Ich finde diese Syntax schmackhaft. Ich glaube nicht, dass ich jemals remove_iffür std :: -Vektoren verwenden würde - es gibt so viel inv.begin()und inv.end()Rauschen, dass Sie wahrscheinlich besser dran sind, einen Ganzzahlindex-basierten Löschvorgang oder nur einen einfachen alten regulären Iterator-basierten Löschvorgang zu verwenden (wie gezeigt) unten). Aber Sie sollten nicht wirklich aus der Mitte eines entfernenstd::vector sowieso sehr viel , daher listwird empfohlen, für diesen Fall des häufigen Löschens in der Mitte der Liste zu einem zu wechseln .

Beachten Sie jedoch, dass ich keine Gelegenheit hatte anzurufen delete auf dem Widget*‚s , die entfernt wurden. Um das zu tun, würde es so aussehen:

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

Sie können auch eine reguläre iteratorbasierte Schleife verwenden:

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

Wenn Sie die Länge von nicht mögen for( list<Widget*>::iterator iter = widgets.begin() ; ..., können Sie verwenden

for( auto iter = widgets.begin() ; ...
Bobobobo
quelle
1
Ich glaube nicht, dass Sie verstehen, wie remove_ifan einem std::vectortatsächlich funktioniert und wie es die Komplexität auf O (N) hält.
Ben Voigt
Das ist egal. Wenn Sie aus der Mitte von a std::vectorentfernen, wird jedes Element immer nach dem Element verschoben, das Sie entfernt haben, wodurch std::listeine viel bessere Wahl getroffen wird.
Bobobobo
1
Nein, es wird nicht "jedes Element um eins nach oben schieben". remove_ifschiebt jedes Element um die Anzahl der freigegebenen Leerzeichen nach oben. Zu der Zeit , erklären Sie Cache - Nutzung, remove_ifauf eine std::vectorwahrscheinlich übertrifft Entfernung von ein std::list. Und bewahrt den O(1)wahlfreien Zugriff.
Ben Voigt
1
Dann haben Sie eine gute Antwort auf die Suche nach einer Frage. Diese Frage befasst sich mit der Iteration der Liste, die für beide Container O (N) ist. Und Entfernen von O (N) -Elementen, die für beide Container ebenfalls O (N) sind.
Ben Voigt
2
Eine Vormarkierung ist nicht erforderlich. Es ist durchaus möglich, dies in einem einzigen Durchgang zu tun. Sie müssen nur den Überblick über "Nächstes zu inspizierendes Element" und "Nächster zu füllender Steckplatz" behalten. Stellen Sie sich vor, Sie erstellen eine Kopie der Liste, die nach dem Prädikat gefiltert wird. Wenn das Prädikat true zurückgibt, überspringen Sie das Element, andernfalls kopieren Sie es. Die Listenkopie wird jedoch erstellt, und anstelle des Kopierens wird das Austauschen / Verschieben verwendet.
Ben Voigt
0

Ich denke, ich würde folgendes tun ...

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;
}
Ajpieri
quelle
0

Sie können den Iterator während der Schleifeniteration nicht löschen, da die Anzahl der Iteratoren nicht übereinstimmt und nach einer gewissen Iteration ein ungültiger Iterator vorliegt.

Lösung: 1) Nehmen Sie die Kopie des Originalvektors. 2) Iterieren Sie den Iterator mit dieser Kopie. 2) Machen Sie einige Dinge und löschen Sie sie aus dem Originalvektor.

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++;
}  
suraj kumar
quelle
0

Das Löschen eines Elements nach dem anderen führt leicht zu einer N ^ 2-Leistung. Markieren Sie besser Elemente, die gelöscht werden sollen, und löschen Sie sie sofort nach der Schleife. Wenn ich nullptr in einem ungültigen Element in Ihrem Vektor annehmen darf, dann

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

sollte arbeiten.

Falls Ihr "Do some stuff" keine Elemente des Vektors ändert und nur dazu dient, die Entscheidung zu treffen, das Element zu entfernen oder beizubehalten, können Sie es in Lambda konvertieren (wie in einem früheren Beitrag von jemandem vorgeschlagen) und verwenden

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 ) );
Sergiy Kanilo
quelle