it-swarm.com.de

Löschen von Elementen aus der STL-Einstellung während der Iteration

Ich muss einen Satz durchgehen und Elemente entfernen, die vordefinierte Kriterien erfüllen.

Dies ist der Testcode, den ich geschrieben habe:

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

Zuerst dachte ich, dass das Löschen eines Elements aus dem Satz während der Iteration den Iterator ungültig machen würde und das Inkrement der for-Schleife ein undefiniertes Verhalten hätte. Obwohl ich diesen Testcode ausgeführt habe, ist alles gut gelaufen und ich kann nicht erklären, warum.

Meine Frage: Ist dies das definierte Verhalten für std-Sets oder ist diese Implementierung spezifisch? Ich verwende übrigens gcc 4.3.3 auf Ubuntu 10.04 (32-Bit-Version).

Vielen Dank!

Vorgeschlagene Lösung:

Ist dies eine korrekte Methode, um Elemente aus dem Satz zu iterieren und zu löschen?

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

Bearbeiten: BEVORZUGTE LÖSUNG

Ich kam um eine Lösung, die mir eleganter erscheint, obwohl sie genau das gleiche tut.

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

Bei mehreren Testbedingungen innerhalb der while-Phase muss jeder von ihnen den Iterator erhöhen. Ich mag diesen Code besser, weil der Iterator nur an einer Stelle erhöht wird, wodurch der Code weniger fehleranfällig und lesbarer wird.

123
pedromanoel

Dies ist von der Implementierung abhängig:

Standard 23.1.2.8:

Die Einfügemitglieder dürfen die Gültigkeit von Iteratoren und Verweisen auf den Container nicht beeinträchtigen, und die Löschmitglieder machen nur Iteratoren und Verweise auf die gelöschten Elemente ungültig.

Vielleicht könnten Sie es versuchen - dies ist standardkonform:

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

Beachten Sie, dass es sich bei ++ um ein Postfix handelt, daher wird die alte Position zum Löschen übergeben, springt jedoch aufgrund des Operators zuerst zu einer neueren Position.

2015.10.27 Update: C++ 11 hat den Fehler behoben. iterator erase (const_iterator position); gibt einen Iterator an das Element zurück, das auf das letzte entfernte Element folgt (oder set :: end, wenn das letzte Element entfernt wurde). C++ 11-Stil ist also:

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

Wenn Sie Ihr Programm über valgrind ausführen, werden einige Lesefehler angezeigt. Mit anderen Worten, ja, die Iteratoren werden ungültig gemacht, aber Sie haben in Ihrem Beispiel Glück (oder wirklich Pech, da Sie die negativen Auswirkungen von undefiniertem Verhalten nicht sehen). Eine Lösung hierfür ist das Erstellen eines temporären Iterators, das Erhöhen der Temperatur, das Löschen des Ziel-Iterators und das Setzen des Ziels auf Temp. Schreiben Sie Ihre Schleife beispielsweise wie folgt neu:

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

Sie verstehen falsch, was "undefiniertes Verhalten" bedeutet. Undefiniertes Verhalten bedeutet nicht "wenn Sie dies tun, stürzt Ihr Programm wird ab oder erzeugt unerwartete Ergebnisse." Dies bedeutet "wenn Sie dies tun, Ihr Programm könnte abstürzen oder unerwartete Ergebnisse erzeugen" oder etwas anderes tun, abhängig von Ihrem Compiler, Ihrem Betriebssystem, der Mondphase usw.

Wenn etwas ohne Absturz ausgeführt wird und sich so verhält, wie Sie es erwarten, ist nicht ein Beweis dafür, dass es sich nicht um ein undefiniertes Verhalten handelt. Alles, was es beweist, ist, dass sein Verhalten nach dem Kompilieren mit dem jeweiligen Compiler unter dem jeweiligen Betriebssystem für diesen bestimmten Lauf beobachtet wurde.

Durch das Löschen eines Elements aus einem Satz wird der Iterator für das gelöschte Element ungültig. Die Verwendung eines ungültigen Iterators ist undefiniertes Verhalten. Es war einfach so, dass das beobachtete Verhalten genau das war, was Sie in diesem speziellen Fall beabsichtigten. Das bedeutet nicht, dass der Code korrekt ist.

6
Tyler McHenry

Um nur zu warnen, dass im Falle eines Deque-Containers alle Lösungen, die die Gleichheit des Deque-Iterators mit numbers.end () prüfen, wahrscheinlich auf gcc 4.8.4 ausfallen. Wenn Sie also ein Element des Deque löschen, wird der Zeiger auf numbers.end () generell ungültig: 

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

Ausgabe:

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

Während die Deque-Transformation in diesem speziellen Fall korrekt ist, wurde der Endzeiger auf dem Weg ungültig gemacht. Bei einem Deque einer anderen Größe wird der Fehler deutlicher:

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

Ausgabe:

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)

Hier ist eine der Möglichkeiten, dies zu beheben:

#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;
      }
    }
  }
}
2
McKryak

Dieses Verhalten ist implementierungsspezifisch. Um die Korrektheit des Iterators zu gewährleisten, sollten Sie "it = numbers.erase (it);" verwenden. Anweisung, wenn Sie das Element löschen und im anderen Fall einfach den Iterator inerieren müssen.

1
Vitaly Bogdanov

Ich denke, die Verwendung der STL-Methode 'remove_if' von könnte dazu beitragen, ein seltsames Problem zu vermeiden, wenn versucht wird, das vom Iterator umschlossene Objekt zu löschen.

Diese Lösung ist möglicherweise weniger effizient.

Nehmen wir an, wir haben eine Art Container wie vector oder eine Liste namens m_bullets:

Bullet::Ptr is a shared_pr<Bullet>

'it' ist der Iterator, den 'remove_if' zurückgibt, das dritte Argument ist eine Lambda-Funktion, die für jedes Element des Containers ausgeführt wird. Da der Container Bullet::Ptr enthält, muss die Lambda-Funktion diesen Typ (oder eine Referenz auf diesen Typ) als Argument übergeben bekommen.

 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' entfernt den Container, in dem die Lambda-Funktion true zurückgegeben hat, und verschiebt den Inhalt an den Anfang des Containers. Die 'it' zeigt auf ein undefiniertes Objekt, das als Abfall betrachtet werden kann. Objekte von 'it' bis m_bullets.end () können gelöscht werden, da sie Speicher belegen, aber Müll enthalten. Daher wird die 'Erase'-Methode für diesen Bereich aufgerufen. 

0
John Behm

C++ 20 wird "einheitliche Containerlöschung" haben, und Sie können schreiben:

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

Und das funktioniert für vector, set, deque, etc . Für weitere Informationen siehe cppReference .

0
Marshall Clow

Ich bin auf dieselbe alte Ausgabe gestoßen und habe unter Code mehr verständlicher gefunden, was in gewisser Weise nach obigen Lösungen ist.

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();
}
0
Anurag