Come rimuovere da una mappa durante l'iterazione?


177

Come rimuovo da una mappa durante l'iterazione? piace:

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

Se lo uso map.eraseinvaliderò gli iteratori





Risposte:


280

Il contenitore associativo standard cancella il linguaggio:

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

Si noti che vogliamo davvero un normale forciclo qui, poiché stiamo modificando il contenitore stesso. Il loop basato sul range dovrebbe essere strettamente riservato alle situazioni in cui ci preoccupiamo solo degli elementi. La sintassi per l'RBFL lo chiarisce senza nemmeno esporre il contenitore all'interno del corpo del loop.

Modificare. Prima di C ++ 11, non era possibile cancellare costanti. Lì dovresti dire:

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

La cancellazione di un elemento da un contenitore non è in contrasto con la costanza dell'elemento. Per analogia, è sempre stato perfettamente legittimo delete pdove si ptrova un puntatore a costante. La costanza non limita la vita; i valori const in C ++ possono ancora smettere di esistere.


1
"nemmeno esporre il contenitore all'interno del corpo del cappio" cosa intendi?
Dani,

2
@Dani: bene, contrappone questo alla costruzione del 20 ° secolo for (int i = 0; i < v.size(); i++). Qui dobbiamo dire v[i]all'interno del ciclo, cioè dobbiamo menzionare esplicitamente il contenitore. D'altra parte, l'RBFL introduce la variabile loop che è direttamente utilizzabile come valore e quindi non è richiesta alcuna conoscenza del contenitore all'interno del loop. Questo è un indizio sull'uso previsto dell'RBFL per i loop che non devono conoscere il contenitore. La cancellazione è la situazione completamente opposta, in cui si tratta del contenitore.
Kerrek SB,

3
@skyhisi: Davvero. Questo è uno degli usi legittimi del post-incremento: Primo incremento itper ottenere il successivo iteratore valido e quindi cancellare quello vecchio. Non funziona al contrario!
Kerrek SB,

5
Ho letto da qualche parte che in C ++ 11 it = v.erase(it);ora funziona anche per le mappe, ovvero, cancella () su tutti gli elementi associativi ora restituisce il successivo iteratore. Quindi il vecchio kludge che richiedeva un post-incremento ++ all'interno di delete (), non è più necessario. Questa (se vera) è una buona cosa, poiché il kludge si basava sulla magia overridden-post-increment-all'interno-di-una-funzione-chiamata, "riparata" dai manutentori principianti per prendere l'incremento dalla chiamata di funzione o per scambiarlo a un preincremento "perché è solo una cosa di stile", ecc.
Dewi Morgan,

3
perché dovresti chiamare it++i blocchi if e else ? non sarebbe sufficiente chiamarlo una volta dopo questi?
nburk,

25

Personalmente preferisco questo modello che è leggermente più chiaro e più semplice, a scapito di una variabile aggiuntiva:

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

Vantaggi di questo approccio:

  • l'incremento for loop ha senso come un incrementatore;
  • l'operazione di cancellazione è una semplice cancellazione, piuttosto che essere mescolata con la logica dell'incremento;
  • dopo la prima riga del corpo del loop, il significato di ite next_itrimanere fisso durante l'iterazione, consentendoti di aggiungere facilmente ulteriori istruzioni che si riferiscono a loro senza fare headcratch sul fatto che funzioneranno come previsto (tranne ovviamente che non puoi usare itdopo averlo cancellato) .

2
Posso pensare a un altro vantaggio in realtà, se il loop chiama in codice che cancella quella voce che viene ripetuta o precedente (e il loop non ne è consapevole) funzionerà senza alcun danno. L'unica limitazione è se qualcosa sta cancellando ciò che viene indicato da next_it o dai successori. È possibile testare anche un elenco / una mappa totalmente cancellati.
Larswad,

Questa risposta è semplice e chiara, anche se il ciclo è più complesso e ha più livelli di logica per decidere se eliminare o meno o fare altre varie attività. Ho proposto una modifica, tuttavia, per renderla un po 'più semplice. "next_it" può essere impostato su "it" in for's init per evitare errori di battitura, e poiché le istruzioni init e iteration impostano entrambi e next_it sugli stessi valori, non è necessario dire "next_it = it;" all'inizio del ciclo.
cdgraham,

1
Tieni presente che chiunque utilizza questa risposta: devi avere "++ next_it" all'interno del ciclo for e non nell'espressione di iterazione. Se si tenta di spostarlo nell'espressione di iterazione come "it = next_it ++", quindi sull'ultima iterazione, quando "it" verrà impostato uguale a "m.cend ()", si tenterà di iterare "next_it" passato "m.cend ()", che è errato.
cdgraham,

6

In breve "Come rimuovo da una mappa durante l'iterazione?"

  • Con la vecchia mappa impl: non puoi
  • Con la nuova mappa impl: quasi come suggerito da @KerrekSB. Ma ci sono alcuni problemi di sintassi in ciò che ha pubblicato.

Da GCC map impl (nota 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

Esempio con vecchio e nuovo stile:

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

stampe:

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.

1
Non capisco Qual è il problema mi.erase(it++);?
lvella,

1
@lvella vedi op. "Se uso map.erase invaliderà gli iteratori".
Kashyap,

Il nuovo metodo non funzionerà se dopo la cancellazione la mappa diventa vuota. In tal caso, l'iteratore verrà invalidato. Quindi, subito dopo la cancellazione, è meglio inserire if(mi.empty()) break;.
Rahat Zaman,

4

La bozza C ++ 20 contiene la funzione convenienza std::erase_if.

Quindi puoi usare quella funzione per farlo come una linea.

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

3

Abbastanza triste, eh? Il modo in cui lo faccio di solito è costruire un contenitore di iteratori invece di eliminarlo durante l'attraversamento. Quindi scorrere il contenitore e utilizzare 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)
}

Ma dopo averne cancellato uno, il resto non sarà più valido
Dani,


@Dani: non in una mappa. La cancellazione nella mappa non fa che invalidare l'iteratore sull'elemento cancellato.
UncleBens,

3

Supponendo C ++ 11, ecco un corpo di loop a una riga, se questo è coerente con il tuo stile di programmazione:

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

Un paio di altri piccoli cambiamenti di stile:

  • Mostra il tipo dichiarato ( Map::const_iterator) quando possibile / conveniente, oltre l'utilizzo auto.
  • Utilizzare usingper i tipi di modello, per rendere Map::const_iteratorpiù facili da leggere / mantenere i tipi ausiliari ( ).
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.