Bagaimana cara menghapus dari peta saat iterasi?

177

Bagaimana cara saya menghapus dari peta saat iterasi? Suka:

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

Jika saya menggunakannya map.eraseakan membatalkan iterators

Dani
sumber
Serupa: stackoverflow.com/questions/1038708/…
Christian Ammer
Bahkan lebih mirip: stackoverflow.com/questions/800955/…
Kleist
Pada topik yang sama: stackoverflow.com/questions/263945/…
Agostino

Jawaban:

279

Ungkapan asosiasi asosiatif-wadah standar:

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

Perhatikan bahwa kami benar-benar menginginkan forloop biasa di sini, karena kami memodifikasi wadah itu sendiri. Lingkaran berbasis rentang harus dicadangkan secara ketat untuk situasi di mana kita hanya peduli pada elemen. Sintaks untuk RBFL membuat ini jelas dengan bahkan tidak mengekspos wadah di dalam tubuh loop.

Edit. Pra-C ++ 11, Anda tidak bisa menghapus const-iterators. Di sana Anda harus mengatakan:

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

Menghapus elemen dari wadah tidak bertentangan dengan ketegasan elemen. Dengan analogi, itu selalu sangat sah di delete pmana ppointer-ke-konstan. Kekekalan tidak membatasi hidup; nilai const di C ++ masih bisa berhenti ada.

Kerrek SB
sumber
1
"bahkan tidak mengekspos wadah di dalam tubuh loop" apa maksudmu?
Dani
2
@Ani: Yah, kontras ini dengan konstruksi abad ke-20 for (int i = 0; i < v.size(); i++). Di sini kita harus mengatakan v[i]di dalam loop, yaitu kita harus secara eksplisit menyebutkan wadah. RBFL di sisi lain memperkenalkan variabel loop yang langsung dapat digunakan sebagai nilai, dan jadi tidak ada pengetahuan tentang kontainer yang diperlukan di dalam loop. Ini adalah petunjuk penggunaan RBFL yang dimaksudkan untuk loop yang tidak perlu tahu tentang wadah. Menghapus adalah situasi yang sangat berlawanan, di mana itu semua tentang wadah.
Kerrek SB
3
@skyhisi: Memang. Ini adalah salah satu kegunaan yang sah dari penambahan setelah penambahan: Penambahan pertamait untuk mendapatkan iterator berikutnya yang valid, dan kemudian menghapus yang lama. Itu tidak bekerja sebaliknya!
Kerrek SB
5
Saya membaca di suatu tempat bahwa di C ++ 11, it = v.erase(it);sekarang berfungsi untuk peta juga. Artinya, hapus () pada semua elemen asosiatif sekarang mengembalikan iterator berikutnya. Jadi kludge lama yang membutuhkan kenaikan pasca ++ dalam delete (), tidak diperlukan lagi. Ini (jika benar) adalah Good Thing, karena kludge mengandalkan sihir pasca-kenaikan-timpa-dalam-fungsi-panggilan, "diperbaiki" oleh pengelola pemula untuk mengambil kenaikan dari panggilan fungsi, atau untuk menukarnya ke preincrement "karena itu hanya hal gaya", dll.
Dewi Morgan
3
mengapa Anda akan menelepon it++dalam if dan else blok? bukankah itu cukup untuk menyebutnya sekali setelah ini?
nburk
25

Saya pribadi lebih suka pola ini yang sedikit lebih jelas dan sederhana, dengan mengorbankan variabel tambahan:

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

Keuntungan dari pendekatan ini:

  • for loop incrementor masuk akal sebagai incrementor;
  • operasi hapus adalah penghapusan sederhana, daripada dicampur dengan logika kenaikan;
  • setelah baris pertama dari badan loop, makna itdan next_ittetap tetap di seluruh iterasi, memungkinkan Anda untuk dengan mudah menambahkan pernyataan tambahan merujuk mereka tanpa headscratching apakah mereka akan berfungsi sebagaimana dimaksud (kecuali tentu saja bahwa Anda tidak dapat menggunakan itsetelah menghapusnya) .
D Coetzee
sumber
2
Saya bisa memikirkan keuntungan lain sebenarnya, jika loop memanggil ke dalam kode yang menghapus entri yang di-iterasi atau yang sebelumnya (dan loop tidak menyadarinya) itu akan bekerja tanpa ada kerusakan yang dilakukan. Satu-satunya batasan adalah apakah sesuatu menghapus apa yang ditunjukkan oleh next_it atau penerusnya. Daftar / peta yang benar-benar bersih dapat diuji terhadapnya juga.
Larswad
Jawaban ini sederhana dan jelas, bahkan jika loop lebih kompleks dan memiliki beberapa level logika untuk memutuskan apakah akan dihapus atau tidak, atau melakukan berbagai tugas lainnya. Saya sudah mengusulkan edit, untuk membuatnya sedikit lebih sederhana. "next_it" dapat diatur ke "it" di in's forit untuk menghindari kesalahan ketik, dan karena pernyataan init dan iterasi keduanya mengatur dan next_it ke nilai yang sama, Anda tidak perlu mengatakan "next_it = it;" di awal loop.
cdgraham
1
Ingatlah siapa pun yang menggunakan jawaban ini: Anda harus memiliki "++ next_it" di dalam for loop, dan tidak dalam ekspresi iterasi. Jika Anda mencoba memindahkannya ke ekspresi iterasi sebagai "it = next_it ++", maka pada iterasi terakhir, ketika "it" akan disetel sama dengan "m.cend ()", Anda akan mencoba untuk mengulang "next_it" melewati "m.cend ()", yang salah.
cdgraham
6

Singkatnya, "Bagaimana saya menghapus dari peta saat iterasi?"

  • Dengan impl peta lama: Anda tidak bisa
  • Dengan impl peta baru: hampir seperti yang disarankan @KerrekSB. Tetapi ada beberapa masalah sintaksis dalam apa yang dia posting.

Dari imp peta GCC (perhatikan 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

Contoh dengan gaya lama dan baru:

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

cetakan:

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
sumber
1
Saya tidak mengerti. Ada apa dengan ini mi.erase(it++);?
lvella
1
@lla melihat op. "Jika saya menggunakan map.erase itu akan membatalkan iterators".
Kashyap
Metode baru Anda tidak akan berfungsi jika setelah dihapus, peta menjadi kosong. Dalam hal ini, iterator akan dibatalkan. Jadi, segera setelah dihapus, lebih baik memasukkan if(mi.empty()) break;.
Rahat Zaman
4

Draf C ++ 20 berisi fungsi kenyamanan std::erase_if.

Jadi Anda bisa menggunakan fungsi itu untuk melakukannya sebagai satu-liner.

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
sumber
3

Cukup sedih, kan? Cara saya biasanya melakukannya adalah membangun wadah iterator bukannya menghapus selama traversal. Kemudian putar melalui wadah dan gunakan 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
sumber
Tetapi setelah menghapus satu sisanya akan tidak valid
Dani
@Dani: Tidak ada di peta. Penghapusan di peta hanya membatalkan iterator ke item yang dihapus.
UncleBens
3

Dengan asumsi C ++ 11, di sini adalah badan loop satu-liner, jika ini konsisten dengan gaya pemrograman Anda:

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

Beberapa perubahan gaya minor lainnya:

  • Tampilkan jenis yang dinyatakan (Map::const_iterator ) bila memungkinkan / nyaman, selama digunakan auto.
  • Gunakan usinguntuk tipe template, untuk membuat tipe tambahan ( Map::const_iterator) lebih mudah dibaca / dipelihara.
Mat
sumber