184 votes

Suppression d'éléments de std::set pendant l'itération

Je dois parcourir un ensemble et supprimer les éléments qui répondent à un critère prédéfini.

Voici 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 que le fait d'effacer un élément de l'ensemble tout en l'itérant invaliderait l'itérateur, et que l'incrément de la boucle for aurait un comportement indéfini. Pourtant, j'ai exécuté ce code de test et tout s'est bien passé, sans que je puisse expliquer pourquoi.

Ma question : S'agit-il du comportement défini pour les ensembles std ou d'une implémentation spécifique ? Au fait, j'utilise gcc 4.3.3 sur ubuntu 10.04 (version 32 bits).

Gracias.

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

J'ai trouvé 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 à l'intérieur du while, chacune d'entre elles doit incrémenter l'itérateur. J'aime mieux ce code car l'itérateur est incrémenté seulement à un endroit ce qui rend le code moins sujet aux erreurs et plus lisible.

1 votes

Questions et réponses : stackoverflow.com/questions/263945/

3 votes

En fait, j'ai lu cette question (et d'autres) avant de poser la mienne, mais comme elles concernaient d'autres conteneurs STL et que mon test initial avait apparemment fonctionné, je pensais qu'il y avait une différence entre elles. Ce n'est qu'après la réponse de Matt que j'ai pensé à utiliser valgrind. Malgré tout, je préfère ma NOUVELLE solution aux autres car elle réduit les risques d'erreurs en incrémentant l'itérateur à un seul endroit. Merci à tous pour votre aide !

1 votes

@pedromanoel ++it devrait être un peu plus efficace que it++ car elle ne nécessite pas l'utilisation d'une copie temporaire invisible de l'itérateur. La version de Kornel assure que les éléments non filtrés sont itérés le plus efficacement possible.

221voto

Kornel Kisielewicz Points 26556

Cela dépend de l'implémentation :

Norme 23.1.2.8 :

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

Peut-être que vous pourriez essayer ceci C'est conforme aux normes :

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

Notez que it++ est postfixe, donc il passe l'ancienne position à effacer, mais saute d'abord à une plus récente grâce à l'opérateur.

Mise à jour 2015.10.27 : C++11 a résolu le défaut. iterator erase (const_iterator position); retourne un itérateur vers l'élément qui suit le dernier élément retiré (ou set::end si le dernier élément a été retiré). Le style C++11 est donc :

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

3 votes

Cela ne fonctionne pas avec deque sur MSVC2013. Soit leur implémentation est boguée, soit il y a encore une autre exigence qui empêche le fonctionnement sur deque . La spécification STL est si compliquée que vous ne pouvez pas vous attendre à ce que toutes les implémentations la suivent, et encore moins à ce que votre programmeur occasionnel la mémorise. La STL est un monstre indomptable, et puisqu'il n'existe pas d'implémentation unique (et que les suites de tests, s'il y en a, ne couvrent apparemment pas des cas aussi évidents que la suppression d'éléments dans une boucle), cela fait de la STL un jouet brillant et fragile qui peut s'envoler avec fracas quand on le regarde de travers.

0 votes

@MatthieuM. C'est le cas en C++11, mais en C++17, il faut un itérateur (const_iterator en C++11).

0 votes

@kuroineko cela ne fonctionnerait pas sur deque car erase invalider tous les itérateurs

22voto

Matt Points 357

Si vous exécutez votre programme avec valgrind, vous verrez un tas d'erreurs de lecture. En d'autres termes, oui, les itérateurs sont invalidés, mais vous êtes chanceux dans votre exemple (ou vraiment malchanceux, car vous ne voyez pas les effets négatifs du comportement non défini). Une solution à ce problème est de créer un itérateur temporaire, d'incrémenter le temporaire, de supprimer l'itérateur cible, puis de définir la cible sur le temporaire. 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;                                                               
    }                                                                       
}

0 votes

Si c'est la seule condition qui compte et qu'elle ne nécessite pas d'initialisation ou de post-opération dans le champ d'application, alors il vaut mieux utiliser while boucle, c'est-à-dire for ( ; it != numbers.end(); ) est mieux visible avec while (it != numbers.end())

8voto

Tyler McHenry Points 35551

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

Si quelque chose s'exécute sans planter et se comporte comme vous l'attendez, c'est que no la preuve 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 celui observé pour cette exécution particulière après avoir été compilé avec ce compilateur particulier sur ce système d'exploitation particulier.

L'effacement d'un élément d'un ensemble invalide l'itérateur de l'élément effacé. L'utilisation d'un itérateur invalidé est un comportement non défini. Il se trouve que le comportement observé correspond à ce que vous vouliez dans ce cas particulier ; cela ne signifie pas que le code est correct.

0 votes

Oh, je suis bien conscient qu'un comportement indéfini peut aussi signifier "Ça marche pour moi, mais pas pour tout le monde". C'est pourquoi j'ai posé cette question, car je ne savais pas si ce comportement était correct ou non. S'il l'était, je le laisserais simplement comme ça. L'utilisation d'une boucle while résoudrait mon problème, alors ? J'ai modifié ma question avec la solution que je propose. Veuillez la consulter.

0 votes

Cela fonctionne pour moi aussi. Mais lorsque je change la condition en if (n > 2 && n < 7 ) alors j'obtiens 0 1 2 4 7 8 9. - Le résultat particulier ici dépend probablement plus des détails d'implémentation de la méthode d'effacement et des itérateurs set, plutôt que de la phase de la lune (non pas que l'on doive jamais se fier aux détails d'implémentation). ;)

2 votes

STL ajoute beaucoup de nouvelles significations à "comportement indéfini". Par exemple, "Microsoft a eu l'idée d'améliorer la spécification en permettant à std::set::erase pour renvoyer un itérateur, de sorte que votre code MSVC se lèvera avec fracas lorsqu'il sera compilé par gcc", ou "Microsoft effectue des vérifications liées sur std::bitset::operator[] Ainsi, votre algorithme de bitset soigneusement optimisé va se retrouver au ralenti lorsqu'il sera compilé avec MSVC". La STL n'a pas d'implémentation unique et sa spécification est un désordre gonflé à croissance exponentielle. Il n'est donc pas étonnant que la suppression d'éléments à l'intérieur d'une boucle nécessite l'expertise d'un programmeur expérimenté...

6voto

Marshall Clow Points 3457

Le C++20 aura un "effacement uniforme des conteneurs", et vous pourrez écrire :

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

Et cela fonctionnera pour vector , set , deque etc. Voir cppRéférence pour plus d'informations.

2voto

McKryak Points 21

Juste pour prévenir, que dans le cas d'un conteneur deque, toutes les solutions qui vérifient l'égalité de l'itérateur deque avec numbers.end() échoueront probablement sous gcc 4.8.4. En effet, l'effacement d'un élément de la deque invalide généralement le pointeur vers 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 si la transformation deque est correcte dans ce cas particulier, le pointeur de fin a été invalidé en cours de route. Avec un deque d'une 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'une des façons 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;
      }
    }
  }
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X