Comment supprimer d'une carte lors de son itération?

177

Comment supprimer une carte lors de son itération? comme:

std::map<K, V> map;
for(auto i : map)
    if(needs_removing(i))
        // remove it from the map

Si je l'utilise, map.erasecela invalidera les itérateurs

Dani
la source
Similaire: stackoverflow.com/questions/1038708/…
Christian Ammer
Encore plus similaire: stackoverflow.com/questions/800955/…
Kleist
Sur le même sujet: stackoverflow.com/questions/263945/…
Agostino

Réponses:

280

L'idiome d'effacement de conteneur associatif standard:

for (auto it = m.cbegin(); it != m.cend() /* not hoisted */; /* no increment */)
{
  if (must_delete)
  {
    m.erase(it++);    // or "it = m.erase(it)" since C++11
  }
  else
  {
    ++it;
  }
}

Notez que nous voulons vraiment une forboucle ordinaire ici, car nous modifions le conteneur lui-même. La boucle basée sur la plage doit être strictement réservée aux situations où nous ne nous soucions que des éléments. La syntaxe du RBFL rend cela clair en n'exposant même pas le conteneur à l'intérieur du corps de la boucle.

Éditer. Avant C ++ 11, vous ne pouviez pas effacer les itérateurs const. Là, il faudrait dire:

for (std::map<K,V>::iterator it = m.begin(); it != m.end(); ) { /* ... */ }

L'effacement d'un élément d'un conteneur n'est pas en contradiction avec la constance de l'élément. Par analogie, il a toujours été parfaitement légitime de savoir delete poù se ptrouve un pointeur vers une constante. La constness ne limite pas la durée de vie; Les valeurs const en C ++ peuvent toujours cesser d'exister.

Kerrek SB
la source
1
"même pas exposer le conteneur à l'intérieur du corps de la boucle" que voulez-vous dire?
Dani
2
@Dani: Eh bien, comparez cela à la construction du XXe siècle for (int i = 0; i < v.size(); i++). Ici, nous devons dire v[i]à l'intérieur de la boucle, c'est-à-dire qu'il faut mentionner explicitement le conteneur. Le RBFL, d'autre part, introduit la variable de boucle qui est directement utilisable comme valeur, et donc aucune connaissance du conteneur n'est requise à l'intérieur de la boucle. Ceci est un indice de l'utilisation prévue du RBFL pour les boucles qui n'ont pas besoin de connaître le conteneur. L'effacement est la situation complètement opposée, où tout tourne autour du conteneur.
Kerrek SB
3
@skyhisi: En effet. C'est l'une des utilisations légitimes du post-incrémentation: d' abord incrémenter itpour obtenir l'itérateur suivant et valide, puis effacer l'ancien. Cela ne fonctionne pas dans l'autre sens!
Kerrek SB
5
J'ai lu quelque part que dans C ++ 11, it = v.erase(it);fonctionne maintenant aussi pour les cartes, c'est-à-dire que erase () sur tous les éléments associatifs renvoie maintenant l'itérateur suivant. Ainsi, l'ancien kludge qui nécessitait un post-incrément ++ dans le delete (), n'est plus nécessaire. Ceci (si c'est vrai) est une bonne chose, car le kludge reposait sur la magie surchargée de post-incrémentation dans un appel de fonction, "corrigée" par les responsables débutants pour supprimer l'incrément de l'appel de fonction, ou pour l'échanger à un pré-incrément "parce que c'est juste une chose de style", etc.
Dewi Morgan
3
pourquoi appelez-vous it++les blocs if et else ? ne serait-il pas suffisant de l'appeler une fois après ceux-ci?
nburk
25

Personnellement, je préfère ce schéma qui est légèrement plus clair et plus simple, au détriment d'une variable supplémentaire:

for (auto it = m.cbegin(), next_it = it; it != m.cend(); it = next_it)
{
  ++next_it;
  if (must_delete)
  {
    m.erase(it);
  }
}

Avantages de cette approche:

  • l'incrémenteur de boucle for a du sens en tant qu'incrémenteur;
  • l'opération d'effacement est un simple effacement, plutôt que d'être mélangée avec une logique d'incrémentation;
  • après la première ligne du corps de la boucle, la signification de itet next_itreste fixe tout au long de l'itération, ce qui vous permet d'ajouter facilement des instructions supplémentaires y faisant référence sans vous demander si elles fonctionneront comme prévu (sauf bien sûr que vous ne pouvez pas l'utiliser itaprès l'avoir effacée) .
D Coetzee
la source
2
Je peux penser à un autre avantage en fait, si la boucle appelle du code qui efface cette entrée en cours d'itération sur ou sur les précédentes (et la boucle n'en est pas consciente), cela fonctionnera sans aucun mal. La seule restriction est de savoir si quelque chose efface ce qui est pointé par next_it ou ses successeurs. Une liste / carte totalement effacée peut également être testée.
Larswad
Cette réponse est simple et claire, même si la boucle est plus complexe et comporte plusieurs niveaux de logique pour décider de supprimer ou non, ou d'effectuer d'autres tâches diverses. J'ai cependant proposé une modification pour le rendre un peu plus simple. "next_it" peut être défini sur "it" dans l'init de for pour éviter les fautes de frappe, et comme les instructions init et iteration définissent à la fois it et next_it sur les mêmes valeurs, vous n'avez pas besoin de dire "next_it = it;" au début de la boucle.
cdgraham
1
Gardez à l'esprit quiconque utilise cette réponse: vous devez avoir "++ next_it" dans la boucle for, et non dans l'expression d'itération. Si vous essayez de le déplacer dans l'expression d'itération en tant que "it = next_it ++", alors à la dernière itération, lorsque "it" sera défini comme égal à "m.cend ()", vous tenterez d'itérer "next_it" passé "m.cend ()", qui est erroné.
cdgraham
6

En bref "Comment supprimer d'une carte lors de son itération?"

  • Avec l'ancienne carte impl: vous ne pouvez pas
  • Avec une nouvelle carte impl: presque comme @KerrekSB l'a suggéré. Mais il y a des problèmes de syntaxe dans ce qu'il a publié.

Depuis GCC map impl (remarque GXX_EXPERIMENTAL_CXX0X ):

#ifdef __GXX_EXPERIMENTAL_CXX0X__
      // _GLIBCXX_RESOLVE_LIB_DEFECTS
      // DR 130. Associative erase should return an iterator.
      /**
       *  @brief Erases an element from a %map.
       *  @param  position  An iterator pointing to the element to be erased.
       *  @return An iterator pointing to the element immediately following
       *          @a position prior to the element being erased. If no such 
       *          element exists, end() is returned.
       *
       *  This function erases an element, pointed to by the given
       *  iterator, from a %map.  Note that this function only erases
       *  the element, and that if the element is itself a pointer,
       *  the pointed-to memory is not touched in any way.  Managing
       *  the pointer is the user's responsibility.
       */
      iterator
      erase(iterator __position)
      { return _M_t.erase(__position); }
#else
      /**
       *  @brief Erases an element from a %map.
       *  @param  position  An iterator pointing to the element to be erased.
       *
       *  This function erases an element, pointed to by the given
       *  iterator, from a %map.  Note that this function only erases
       *  the element, and that if the element is itself a pointer,
       *  the pointed-to memory is not touched in any way.  Managing
       *  the pointer is the user's responsibility.
       */
      void
      erase(iterator __position)
      { _M_t.erase(__position); }
#endif

Exemple avec l'ancien et le nouveau style:

#include <iostream>
#include <map>
#include <vector>
#include <algorithm>

using namespace std;
typedef map<int, int> t_myMap;
typedef vector<t_myMap::key_type>  t_myVec;

int main() {

    cout << "main() ENTRY" << endl;

    t_myMap mi;
    mi.insert(t_myMap::value_type(1,1));
    mi.insert(t_myMap::value_type(2,1));
    mi.insert(t_myMap::value_type(3,1));
    mi.insert(t_myMap::value_type(4,1));
    mi.insert(t_myMap::value_type(5,1));
    mi.insert(t_myMap::value_type(6,1));

    cout << "Init" << endl;
    for(t_myMap::const_iterator i = mi.begin(); i != mi.end(); i++)
        cout << '\t' << i->first << '-' << i->second << endl;

    t_myVec markedForDeath;

    for (t_myMap::const_iterator it = mi.begin(); it != mi.end() ; it++)
        if (it->first > 2 && it->first < 5)
            markedForDeath.push_back(it->first);

    for(size_t i = 0; i < markedForDeath.size(); i++)
        // old erase, returns void...
        mi.erase(markedForDeath[i]);

    cout << "after old style erase of 3 & 4.." << endl;
    for(t_myMap::const_iterator i = mi.begin(); i != mi.end(); i++)
        cout << '\t' << i->first << '-' << i->second << endl;

    for (auto it = mi.begin(); it != mi.end(); ) {
        if (it->first == 5)
            // new erase() that returns iter..
            it = mi.erase(it);
        else
            ++it;
    }

    cout << "after new style erase of 5" << endl;
    // new cend/cbegin and lambda..
    for_each(mi.cbegin(), mi.cend(), [](t_myMap::const_reference it){cout << '\t' << it.first << '-' << it.second << endl;});

    return 0;
}

imprime:

main() ENTRY
Init
        1-1
        2-1
        3-1
        4-1
        5-1
        6-1
after old style erase of 3 & 4..
        1-1
        2-1
        5-1
        6-1
after new style erase of 5
        1-1
        2-1
        6-1

Process returned 0 (0x0)   execution time : 0.021 s
Press any key to continue.
Kashyap
la source
1
Je ne comprends pas. Quel est le problème mi.erase(it++);?
lvella
1
@lvella voir op. "Si j'utilise map.erase, cela invalidera les itérateurs".
Kashyap
Votre nouvelle méthode ne fonctionnera pas si, après l'effacement, la carte devient vide. Dans ce cas, l'itérateur sera invalidé. Donc, peu de temps après l'effacement, il est préférable d'insérer if(mi.empty()) break;.
Rahat Zaman
4

Le projet C ++ 20 contient la fonction de commodité std::erase_if.

Vous pouvez donc utiliser cette fonction pour le faire comme une seule ligne.

std::map<K, V> map_obj;
//calls needs_removing for each element and erases it, if true was reuturned
std::erase_if(map_obj,needs_removing);
//if you need to pass only part of the key/value pair
std::erase_if(map_obj,[](auto& kv){return needs_removing(kv.first);});
PeterT
la source
3

Assez triste, hein? La façon dont je le fais habituellement est de créer un conteneur d'itérateurs au lieu de les supprimer pendant la traversée. Ensuite, parcourez le conteneur et utilisez map.erase ()

std::map<K,V> map;
std::list< std::map<K,V>::iterator > iteratorList;

for(auto i : map ){
    if ( needs_removing(i)){
        iteratorList.push_back(i);
    }
}
for(auto i : iteratorList){
    map.erase(*i)
}
Michael Daum
la source
Mais après en avoir effacé un, le reste sera invalide
Dani
@Dani: Pas sur une carte. L'effacement dans la carte invalide uniquement l'itérateur de l'élément effacé.
UncleBens
3

En supposant C ++ 11, voici un corps de boucle à une seule ligne, si cela est cohérent avec votre style de programmation:

using Map = std::map<K,V>;
Map map;

// Erase members that satisfy needs_removing(itr)
for (Map::const_iterator itr = map.cbegin() ; itr != map.cend() ; )
  itr = needs_removing(itr) ? map.erase(itr) : std::next(itr);

Quelques autres changements de style mineurs:

  • Afficher le type déclaré ( Map::const_iterator) lorsque cela est possible / pratique, en utilisant auto.
  • Utilisez usingpour les types de modèles, pour rendre les types auxiliaires ( Map::const_iterator) plus faciles à lire / maintenir.
Mat
la source