web-dev-qa-db-fra.com

Suppression d'éléments du jeu STL lors de l'itération

Je dois parcourir un ensemble et supprimer des éléments répondant à des critères prédéfinis.

C'est le code de test que j'ai écrit:

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

Au début, je pensais qu'effacer un élément de l'ensemble en l'itérant invaliderait l'itérateur et que l'incrémentation dans la boucle for aurait un comportement indéfini. Même si j’ai exécuté ce code de test et que tout s’est bien passé, je ne peux pas expliquer pourquoi.

Ma question: S'agit-il du comportement défini pour les ensembles std ou cette implémentation est-elle spécifique? J'utilise gcc 4.3.3 sur Ubuntu 10.04 (version 32 bits), soit dit en passant.

Merci!

Solution proposée:

Est-ce une façon correcte d'itérer et d'effacer des éléments de l'ensemble?

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

Edit: SOLUTION PRÉFÉRÉE

Je suis arrivé à une solution qui me semble plus élégante, même si elle fait exactement la même chose.

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

S'il y a plusieurs conditions de test dans la durée, chacune d'elles doit incrémenter l'itérateur. J'aime mieux ce code car l'itérateur est incrémenté uniquement à un endroit , ce qui rend le code moins sujet aux erreurs et plus lisible.

123
pedromanoel

Ceci dépend de la mise en œuvre:

Norme 23.1.2.8:

Les membres insérés ne doivent pas affecter la validité des itérateurs et des références au conteneur, et les membres effacés ne doivent invalider que les itérateurs et les références aux éléments effacés.

Peut-être que vous pourriez essayer ceci - ceci est conforme à la norme:

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

Notez que ce ++ est postfixé, il passe donc l'ancienne position à effacer, mais passe tout d'abord à une plus récente en raison de l'opérateur.

Mise à jour 2015.10.27: C++ 11 a résolu le problème. iterator erase (const_iterator position); renvoie un itérateur à l'élément qui suit le dernier élément supprimé (ou set :: end, si le dernier élément a été supprimé). Donc, le style C++ 11 est:

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

Si vous exécutez votre programme via valgrind, vous verrez un tas d'erreurs de lecture. En d'autres termes, oui, les itérateurs sont invalidés, mais votre exemple est chanceux (ou vraiment malchanceux, car vous ne voyez pas les effets négatifs d'un comportement non défini). Une solution consiste à créer un itérateur temporaire, à incrémenter le temp, à supprimer l'itérateur de cible, puis à définir la cible sur temp. Par exemple, réécrivez votre boucle comme suit:

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

Vous comprenez mal ce que signifie "comportement indéfini". Un comportement indéfini ne signifie pas "si vous faites cela, votre programme va planter ou produire des résultats inattendus." Cela signifie "si vous faites cela, votre programme pourrait planter ou produire des résultats inattendus", ou faire autre chose, selon votre compilateur, votre système d'exploitation, la phase de la lune, etc.

Si quelque chose s'exécute sans se planter et se comporte comme prévu, c'est la preuve pas qu'il ne s'agit pas d'un comportement indéfini. Tout ce que cela prouve, c'est que son comportement s'est avéré être le même que celui observé pour cette exécution particulière après la compilation avec ce compilateur particulier sur ce système d'exploitation particulier.

Effacer un élément d'un ensemble invalide l'itérateur de l'élément effacé. Utiliser un itérateur invalidé est un comportement indéfini. Il se trouve que le comportement observé correspond à ce que vous souhaitiez dans ce cas particulier. cela ne signifie pas que le code est correct.

6
Tyler McHenry

Juste pour avertir que dans le cas d'un conteneur deque, toutes les solutions qui vérifient l'égalité d'égalité d'itérateur de nombres avec numers.end () échoueront probablement sur gcc 4.8.4. À savoir, effacer un élément de la déque invalide généralement le pointeur sur 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;
    }
  }
}

Sortie:

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

Notez que bien que la transformation deque soit correcte dans ce cas particulier, le pointeur de fin a été invalidé en cours de route. Avec le deque de taille différente, l'erreur est plus apparente:

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

Sortie:

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)

Voici l'un des moyens de résoudre ce problème:

#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

Ce comportement est spécifique à l'implémentation. Pour garantir l'exactitude de l'itérateur, vous devez utiliser "it = numbers.erase (it);" instruction si vous devez supprimer l'élément et simplement inciter l'itérateur dans les autres cas.

1
Vitaly Bogdanov

Je pense que l’utilisation de la méthode STL 'remove_if' de pourrait aider à éviter un problème étrange lorsque vous essayez de supprimer l’objet encapsulé par l’itérateur.

Cette solution peut être moins efficace.

Disons que nous avons une sorte de conteneur, comme un vecteur ou une liste appelée m_bullets:

Bullet::Ptr is a shared_pr<Bullet>

'it' est l'itérateur renvoyé par 'remove_if', le troisième argument est une fonction lambda qui est exécutée sur chaque élément du conteneur. Comme le conteneur contient Bullet::Ptr, la fonction lambda doit obtenir ce type (ou une référence à ce type) passé en tant qu'argument.

 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' supprime le conteneur dans lequel la fonction lambda a renvoyé la valeur true et déplace ce contenu au début du conteneur. La variable 'it' pointe vers un objet non défini pouvant être considéré comme un déchet. Les objets de 'it' à m_bullets.end () peuvent être effacés, car ils occupent de la mémoire, mais contiennent des ordures, ainsi la méthode 'erase' est appelée sur cette plage. 

0
John Behm

C++ 20 aura "l'effacement uniforme du conteneur", et vous pourrez écrire:

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

Et cela fonctionnera pour vector, set, deque, etc . Voir cppReference pour plus d’informations.

0
Marshall Clow

Je suis tombé sur le même problème et j'ai trouvé le code ci-dessous plus compréhensible qui correspond en quelque sorte aux solutions susmentionnées.

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