Bisakah Anda menghapus elemen dari daftar std :: saat iterasi melalui itu?

239

Saya punya kode yang terlihat seperti ini:

for (std::list<item*>::iterator i=items.begin();i!=items.end();i++)
{
    bool isActive = (*i)->update();
    //if (!isActive) 
    //  items.remove(*i); 
    //else
       other_code_involving(*i);
}
items.remove_if(CheckItemNotActive);

Saya ingin menghapus item yang tidak aktif segera setelah memperbaruinya, inorder untuk menghindari berjalan daftar lagi. Tetapi jika saya menambahkan baris komentar, saya mendapatkan kesalahan ketika saya sampai ke i++: "Daftar iterator not incrementable". Saya mencoba beberapa alternatif yang tidak bertambah dalam pernyataan untuk, tapi saya tidak bisa mendapatkan apa pun untuk bekerja.

Apa cara terbaik untuk menghapus item saat Anda berjalan di std :: list?

ASHelly
sumber
Saya belum melihat solusi apa pun berdasarkan iterasi mundur. Saya memposting satu seperti itu .
sancho.s ReinstateMonicaCellio

Jawaban:

286

Anda harus menambahkan iterator terlebih dahulu (dengan i ++) dan kemudian menghapus elemen sebelumnya (misalnya, dengan menggunakan nilai yang dikembalikan dari i ++). Anda dapat mengubah kode menjadi loop sementara seperti:

std::list<item*>::iterator i = items.begin();
while (i != items.end())
{
    bool isActive = (*i)->update();
    if (!isActive)
    {
        items.erase(i++);  // alternatively, i = items.erase(i);
    }
    else
    {
        other_code_involving(*i);
        ++i;
    }
}
Michael Kristofik
sumber
7
Sebenarnya, itu tidak dijamin berhasil. Dengan "erase (i ++);", kita hanya tahu bahwa nilai pre-incremented diteruskan ke erase (), dan i ditambahkan sebelum semi-kolon, tidak harus sebelum panggilan untuk menghapus (). "iterator prev = i ++; erase (prev);" pasti bekerja, seperti menggunakan nilai kembali
James Curran
58
Tidak James, saya bertambah sebelum memanggil hapus, dan nilai sebelumnya dilewatkan ke fungsi. Argumen fungsi harus sepenuhnya dievaluasi sebelum fungsi dipanggil.
Brian Neal
28
@ James Curran: Itu tidak benar. SEMUA argumen sepenuhnya dievaluasi sebelum suatu fungsi dipanggil.
Martin York
9
Martin York benar. Semua argumen untuk panggilan fungsi sepenuhnya dievaluasi sebelum suatu fungsi dipanggil, tanpa kecuali. Itulah cara kerja fungsi. Dan itu tidak ada hubungannya dengan contoh foo.b (i ++) Anda. C (i ++) (yang tidak ditentukan dalam hal apa pun)
jalf
75
Penggunaan alternatif i = items.erase(i)lebih aman, karena setara dengan daftar, tetapi akan tetap berfungsi jika seseorang mengubah wadah ke vektor. Dengan vektor, hapus () memindahkan semuanya ke kiri untuk mengisi lubang. Jika Anda mencoba untuk menghapus item terakhir dengan kode yang menambah iterator setelah dihapus, ujungnya bergerak ke kiri, dan iterator bergerak ke kanan - melewati ujungnya. Dan kemudian Anda jatuh.
Eric Seppanen
133

Anda ingin melakukan:

i= items.erase(i);

Itu akan benar memperbarui iterator untuk menunjuk ke lokasi setelah iterator Anda dihapus.

MSN
sumber
80
Berhati-hatilah bahwa Anda tidak bisa hanya memasukkan kode itu ke dalam for-loop Anda. Kalau tidak, Anda akan melewatkan elemen setiap kali Anda menghapusnya.
Michael Kristofik
2
bisakah dia tidak melakukan saya--; setiap kali mengikuti sepotong kode untuk menghindari melewatkan?
Antusiasme
1
@enthusiasticgeek, apa yang terjadi jika i==items.begin()?
MSN
1
@enthusiasticgeek, pada saat itu Anda hanya perlu melakukannya i= items.erase(i);. Ini adalah bentuk kanonik dan sudah mengurus semua perincian itu.
MSN
6
Michael menunjukkan 'gotcha' besar, aku harus berurusan dengan hal yang sama barusan. Metode termudah untuk menghindarinya yang saya temukan hanya menguraikan loop for () menjadi sementara () dan berhati-hati dengan penambahan
Anne Quinn
22

Anda perlu melakukan kombinasi jawaban Kristo dan MSN:

// Note: Using the pre-increment operator is preferred for iterators because
//       there can be a performance gain.
//
// Note: As long as you are iterating from beginning to end, without inserting
//       along the way you can safely save end once; otherwise get it at the
//       top of each loop.

std::list< item * >::iterator iter = items.begin();
std::list< item * >::iterator end  = items.end();

while (iter != end)
{
    item * pItem = *iter;

    if (pItem->update() == true)
    {
        other_code_involving(pItem);
        ++iter;
    }
    else
    {
        // BTW, who is deleting pItem, a.k.a. (*iter)?
        iter = items.erase(iter);
    }
}

Tentu saja, hal yang paling efisien dan SuperCool® STL akan seperti ini:

// This implementation of update executes other_code_involving(Item *) if
// this instance needs updating.
//
// This method returns true if this still needs future updates.
//
bool Item::update(void)
{
    if (m_needsUpdates == true)
    {
        m_needsUpdates = other_code_involving(this);
    }

    return (m_needsUpdates);
}

// This call does everything the previous loop did!!! (Including the fact
// that it isn't deleting the items that are erased!)
items.remove_if(std::not1(std::mem_fun(&Item::update)));
Mike
sumber
Saya memang mempertimbangkan metode SuperCool Anda, keraguan saya adalah bahwa panggilan untuk remove_if tidak menjelaskan bahwa tujuannya adalah untuk memproses item, daripada menghapusnya dari daftar yang aktif. (Item tidak dihapus karena mereka hanya menjadi tidak aktif, bukan tidak dibutuhkan)
AShelly
Saya kira kamu benar. Di satu sisi saya cenderung menyarankan mengubah nama 'pembaruan' untuk menghapus ketidakjelasan, tetapi kenyataannya adalah, kode ini suka dengan functors, tetapi juga apa pun selain tidak jelas.
Mike
Komentar wajar, perbaiki loop sementara untuk menggunakan akhir atau menghapus definisi yang tidak digunakan.
Mike
10

Gunakan algoritma std :: remove_if.

Edit: Bekerja dengan koleksi harus seperti: 1. menyiapkan koleksi. 2. proses pengumpulan.

Hidup akan lebih mudah jika Anda tidak akan mencampur langkah-langkah ini.

  1. std :: remove_if. atau daftar :: remove_if (jika Anda tahu bahwa Anda bekerja dengan daftar dan tidak dengan TCollection)
  2. std :: for_each
Mykola Golubyev
sumber
2
std :: list memiliki fungsi anggota remove_if yang lebih efisien daripada algoritma remove_if (dan tidak memerlukan idiom "hapus-hapus").
Brian Neal
5

Berikut ini contoh menggunakan forloop yang mengulang daftar dan menambah atau memvalidasi ulang iterator jika ada item yang dihapus selama traversal daftar.

for(auto i = items.begin(); i != items.end();)
{
    if(bool isActive = (*i)->update())
    {
        other_code_involving(*i);
        ++i;

    }
    else
    {
        i = items.erase(i);

    }

}

items.remove_if(CheckItemNotActive);
David Cormack
sumber
4

Alternatif untuk versi loop ke jawaban Kristo.

Anda kehilangan beberapa efisiensi, Anda mundur dan maju lagi ketika menghapus tetapi dengan imbalan kenaikan iterator tambahan Anda dapat memiliki iterator dideklarasikan dalam lingkup loop dan kode tampak sedikit lebih bersih. Apa yang harus dipilih tergantung pada prioritas saat ini.

Jawabannya benar-benar kehabisan waktu, saya tahu ...

typedef std::list<item*>::iterator item_iterator;

for(item_iterator i = items.begin(); i != items.end(); ++i)
{
    bool isActive = (*i)->update();

    if (!isActive)
    {
        items.erase(i--); 
    }
    else
    {
        other_code_involving(*i);
    }
}
Rafael Gago
sumber
1
Itu yang saya gunakan juga. Tapi saya tidak yakin apakah itu dijamin berfungsi jika elemen yang akan dihapus adalah elemen pertama dalam wadah. Bagi saya, itu berfungsi, saya pikir, tetapi saya tidak yakin apakah ini portable di seluruh platform.
trololo
Saya tidak melakukan "-1", namun, daftar iterator tidak dapat mengurangi? Setidaknya saya mendapat penegasan dari Visual Studio 2008.
milesma
Selama daftar tertaut diimplementasikan sebagai daftar dobel melingkar yang memiliki simpul head / stub (digunakan sebagai end () rbegin () dan ketika kosong digunakan sebagai begin () dan rend () juga) ini akan berfungsi. Saya tidak ingat di platform mana saya menggunakan ini, tetapi itu bekerja untuk saya juga, karena implementasi yang disebutkan di atas adalah implementasi yang paling umum untuk std :: list. Tetapi bagaimanapun juga, ini hampir pasti bahwa ini mengeksploitasi beberapa perilaku yang tidak terdefinisi (dengan standar C ++), jadi lebih baik jangan menggunakannya.
Rafael Gago
re: iterator cannot be decrementedThe eraseMetode membutuhkan random access iterator. Beberapa implementasi pengumpulan menyediakan suatu forward only iteratoryang menyebabkan penegasan.
Jesse Chisholm
@Jesse Chisholm pertanyaannya adalah tentang std :: daftar, bukan wadah yang sewenang-wenang. std :: list menyediakan penghapusan dan pengulang dua arah.
Rafael Gago
4

Saya sudah simpulkan, berikut adalah tiga metode dengan contoh:

1. menggunakan whileloop

list<int> lst{4, 1, 2, 3, 5};

auto it = lst.begin();
while (it != lst.end()){
    if((*it % 2) == 1){
        it = lst.erase(it);// erase and go to next
    } else{
        ++it;  // go to next
    }
}

for(auto it:lst)cout<<it<<" ";
cout<<endl;  //4 2

2. menggunakan remove_iffuntion anggota dalam daftar:

list<int> lst{4, 1, 2, 3, 5};

lst.remove_if([](int a){return a % 2 == 1;});

for(auto it:lst)cout<<it<<" ";
cout<<endl;  //4 2

3. menggunakan std::remove_ifkombinasi funtion dengan erasefungsi anggota:

list<int> lst{4, 1, 2, 3, 5};

lst.erase(std::remove_if(lst.begin(), lst.end(), [](int a){
    return a % 2 == 1;
}), lst.end());

for(auto it:lst)cout<<it<<" ";
cout<<endl;  //4 2

4. menggunakan forloop, harus perhatikan pembaruan iterator:

list<int> lst{4, 1, 2, 3, 5};

for(auto it = lst.begin(); it != lst.end();++it){
    if ((*it % 2) == 1){
        it = lst.erase(it);  erase and go to next(erase will return the next iterator)
        --it;  // as it will be add again in for, so we go back one step
    }
}

for(auto it:lst)cout<<it<<" ";
cout<<endl;  //4 2 
Jayhello
sumber
2

Penghapusan hanya membatalkan iterator yang menunjuk ke elemen yang dihapus.

Jadi dalam hal ini setelah menghapus * i, saya tidak valid dan Anda tidak dapat melakukan peningkatan padanya.

Yang bisa Anda lakukan adalah pertama-tama menyimpan iterator elemen yang akan dihapus, kemudian menambahkan iterator dan kemudian menghapus yang disimpan.

anand
sumber
2
Menggunakan post-increment jauh lebih elegan.
Brian Neal
2

Jika Anda berpikir tentang std::listantrian seperti, maka Anda dapat mengeluarkan dan mengirimkan semua item yang ingin Anda simpan, tetapi hanya dequeue (dan bukan enqueue) item yang ingin Anda hapus. Berikut adalah contoh di mana saya ingin menghapus 5 dari daftar yang berisi angka 1-10 ...

std::list<int> myList;

int size = myList.size(); // The size needs to be saved to iterate through the whole thing

for (int i = 0; i < size; ++i)
{
    int val = myList.back()
    myList.pop_back() // dequeue
    if (val != 5)
    {
         myList.push_front(val) // enqueue if not 5
    }
}

myList sekarang hanya akan memiliki angka 1-4 dan 6-10.

Alex Bagg
sumber
Pendekatan yang menarik tetapi saya takut itu mungkin lambat.
sg7
2

Iterasi mundur menghindari efek menghapus elemen pada elemen yang tersisa untuk dilalui:

typedef list<item*> list_t;
for ( list_t::iterator it = items.end() ; it != items.begin() ; ) {
    --it;
    bool remove = <determine whether to remove>
    if ( remove ) {
        items.erase( it );
    }
}

PS: lihat ini , misalnya, tentang iterasi mundur.

PS2: Saya tidak diuji secara menyeluruh jika menangani elemen yang dihapus dengan baik di ujungnya.

sancho.s ReinstateMonicaCellio
sumber
re: avoids the effect of erasing an element on the remaining elementsuntuk daftar, mungkin ya. Untuk vektor mungkin tidak. Itu bukan sesuatu yang dijamin pada koleksi sewenang-wenang. Misalnya, peta dapat memutuskan untuk menyeimbangkan dirinya sendiri.
Jesse Chisholm
1

Kamu bisa menulis

std::list<item*>::iterator i = items.begin();
while (i != items.end())
{
    bool isActive = (*i)->update();
    if (!isActive) {
        i = items.erase(i); 
    } else {
        other_code_involving(*i);
        i++;
    }
}

Anda dapat menulis kode setara dengan std::list::remove_if, yang kurang verbose dan lebih eksplisit

items.remove_if([] (item*i) {
    bool isActive = (*i)->update();
    if (!isActive) 
        return true;

    other_code_involving(*i);
    return false;
});

The std::vector::erase std::remove_ifidiom harus digunakan bila item adalah vektor bukan daftar untuk menjaga compexity di O (n) - atau jika Anda menulis kode generik dan item mungkin wadah dengan tidak ada cara yang efektif untuk menghapus item tunggal (seperti vektor)

items.erase(std::remove_if(begin(items), end(items), [] (item*i) {
    bool isActive = (*i)->update();
    if (!isActive) 
        return true;

    other_code_involving(*i);
    return false;
}));
J. Kalz
sumber
-4

Saya pikir Anda memiliki bug di sana, saya kode dengan cara ini:

for (std::list<CAudioChannel *>::iterator itAudioChannel = audioChannels.begin();
             itAudioChannel != audioChannels.end(); )
{
    CAudioChannel *audioChannel = *itAudioChannel;
    std::list<CAudioChannel *>::iterator itCurrentAudioChannel = itAudioChannel;
    itAudioChannel++;

    if (audioChannel->destroyMe)
    {
        audioChannels.erase(itCurrentAudioChannel);
        delete audioChannel;
        continue;
    }
    audioChannel->Mix(outBuffer, numSamples);
}
Marcin Skoczylas
sumber
Saya menduga ini diturunkan untuk preferensi gaya, karena tampaknya fungsional. Ya, tentu saja, (1) ia menggunakan iterator tambahan, (2) kenaikan iterator berada di tempat yang aneh untuk sebuah loop tanpa alasan yang baik untuk meletakkannya di sana, (3) ia melakukan pekerjaan saluran setelah keputusan untuk menghapus sebagai gantinya dari sebelumnya suka di OP. Tapi itu bukan jawaban yang salah.
Jesse Chisholm