Jak usunąć z mapy podczas iteracji?


177

Jak usunąć z mapy podczas iteracji? lubić:

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

Jeśli map.erasego użyję, unieważni iteratory



Jeszcze bardziej podobny: stackoverflow.com/questions/800955/…
Kleist


Odpowiedzi:


280

Standardowy idiom usuwania kontenerów asocjacyjnych:

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

Zwróć uwagę, że naprawdę chcemy fortutaj zwykłej pętli, ponieważ modyfikujemy sam kontener. Pętla zakresowa powinna być zarezerwowana dla sytuacji, w których zależy nam tylko na elementach. Składnia RBFL wyjaśnia to jasno, nawet nie ujawniając kontenera wewnątrz ciała pętli.

Edytować. Przed C ++ 11 nie można było usunąć stałych iteratorów. Tam musiałbyś powiedzieć:

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

Usunięcie elementu z kontenera nie jest sprzeczne ze stałością elementu. Analogicznie, zawsze było całkowicie uzasadnione, delete pgdzie pznajduje się wskaźnik do stałej. Stałość nie ogranicza życia; wartości const w C ++ mogą nadal przestać istnieć.


1
„nawet nie wystawiając pojemnika wewnątrz korpusu pętli” co masz na myśli?
Dani

2
@Dani: Cóż, porównajmy to z konstrukcją z XX wieku for (int i = 0; i < v.size(); i++). Tutaj musimy powiedzieć v[i]wewnątrz pętli, czyli musimy wyraźnie wspomnieć o kontenerze. Z drugiej strony RBFL wprowadza zmienną pętli, która jest bezpośrednio używana jako wartość, więc nie jest wymagana znajomość kontenera wewnątrz pętli. Jest to wskazówka co do zamierzonego użycia RBFL dla pętli, które nie muszą wiedzieć o kontenerze. Kasowanie to zupełnie odwrotna sytuacja, w której chodzi o pojemnik.
Kerrek SB,

3
@skyhisi: Rzeczywiście. Jest to jedno z legalnych zastosowań post-inkrementacji: najpierw inkrementacja w itcelu uzyskania następnego, prawidłowego iteratora, a następnie usunięcie starego. To nie działa w drugą stronę!
Kerrek SB,

5
Czytałem gdzieś, że w C ++ 11, it = v.erase(it);teraz działa też dla map, czyli erase () na wszystkich elementach asocjacyjnych zwraca teraz następny iterator. Więc stary kludge, który wymagał post-inkrementacji ++ w ramach delete (), nie jest już potrzebny. To (jeśli prawda) jest Dobra Rzecz, ponieważ kludge polegał na zastępowanej magii wywołania funkcji po inkrementacji, „naprawionej” przez początkujących opiekunów, aby usunąć przyrost z wywołania funkcji lub zamienić go do preinkrement „bo to tylko kwestia stylu” itp.
Dewi Morgan,

3
dlaczego miałbyś dzwonić it++do bloków if i else ? czy nie wystarczy nazwać to raz po tych?
nburk

25

Osobiście wolę ten wzór, który jest nieco jaśniejszy i prostszy kosztem dodatkowej zmiennej:

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

Zalety tego podejścia:

  • element zwiększający pętlę for ma sens jako element zwiększający;
  • operacja kasowania jest prostym kasowaniem, a nie jest mieszana z logiką inkrementacji;
  • po pierwszym wierszu ciała pętli, znaczenie iti next_itpozostaje niezmienne przez całą iterację, co pozwala na łatwe dodawanie dodatkowych instrukcji odnoszących się do nich bez zastanawiania się, czy będą działać zgodnie z przeznaczeniem (z wyjątkiem oczywiście, że nie można ich użyć itpo skasowaniu) .

2
Mogę wymyślić inną zaletę, jeśli pętla wywołuje kod, który usuwa iterację tego wpisu lub poprzednie (a pętla nie jest tego świadoma), będzie działać bez żadnych szkód. Jedynym ograniczeniem jest to, czy coś wymazuje to, na co wskazuje next_it lub następcy. Całkowicie wyczyszczoną listę / mapę można również przetestować.
Larswad

Ta odpowiedź jest prosta i jasna, nawet jeśli pętla jest bardziej złożona i ma wiele poziomów logiki, aby zdecydować, czy usunąć, czy wykonać inne różne zadania. Zaproponowałem jednak zmianę, aby było to trochę prostsze. "next_it" można ustawić na "it" w init for, aby uniknąć literówek, a ponieważ zarówno instrukcje init, jak i iteracji ustawiają go i next_it na te same wartości, nie trzeba mówić "next_it = it;" na początku pętli.
cdgraham

1
Pamiętaj o każdym, kto użyje tej odpowiedzi: Musisz mieć „++ next_it” wewnątrz pętli for, a nie w wyrażeniu iteracji. Jeśli spróbujesz przenieść to do wyrażenia iteracji jako „it = next_it ++”, to w ostatniej iteracji, gdy „it” zostanie ustawione na „m.cend ()”, spróbujesz wykonać iterację „next_it” przeszłość „m.cend ()”, która jest błędna.
cdgraham

6

W skrócie „Jak usunąć z mapy podczas iteracji?”

  • Ze starą mapą impl: Nie możesz
  • Z nową mapą impl: prawie tak, jak sugerował @KerrekSB. Ale są pewne problemy ze składnią w tym, co opublikował.

Z impl mapy GCC (uwaga 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

Przykład ze starym i nowym stylem:

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

wydruki:

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
Nie rozumiem. Z czym jest problem mi.erase(it++);?
lvella

1
@lvella patrz op. „Jeśli użyję map.erase, spowoduje to unieważnienie iteratorów”.
Kashyap

Twoja nowa metoda nie zadziała, jeśli po wymazaniu mapa stanie się pusta. W takim przypadku iterator zostanie unieważniony. Więc wkrótce po skasowaniu lepiej jest wstawić if(mi.empty()) break;.
Rahat Zaman

4

Wersja robocza C ++ 20 zawiera wygodną funkcję std::erase_if.

Możesz więc użyć tej funkcji, aby zrobić to jako jedną linijkę.

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

Całkiem smutne, co? Sposób, w jaki zwykle to robię, to budowanie kontenera iteratorów zamiast usuwania podczas przechodzenia. Następnie przejdź przez kontener i użyj 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)
}

Ale po usunięciu jednego reszta będzie nieważna
Dani


@Dani: Nie ma na mapie. Wymazanie na mapie unieważnia tylko iterator do wymazanego elementu.
UncleBens

3

Zakładając C ++ 11, oto treść pętli jednowierszowej, jeśli jest to zgodne z Twoim stylem programowania:

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

Kilka innych drobnych zmian w stylu:

  • Pokaż zadeklarowany typ ( Map::const_iterator), jeśli to możliwe / wygodne, przed użyciem auto.
  • Użyj usingdla typów szablonów, aby Map::const_iteratorułatwić odczytywanie / utrzymywanie typów pomocniczych ( ).
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.