Yineleme sırasında std :: set'den eleman silme


149

Bir kümeden geçmem ve önceden tanımlanmış bir kriteri karşılayan öğeleri kaldırmam gerekiyor.

Bu yazdığım test kodu:

#include <set>
#include <algorithm>

void printElement(int value) {
    std::cout << value << " ";
}

int main() {
    int initNum[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    std::set<int> numbers(initNum, initNum + 10);
    // print '0 1 2 3 4 5 6 7 8 9'
    std::for_each(numbers.begin(), numbers.end(), printElement);

    std::set<int>::iterator it = numbers.begin();

    // iterate through the set and erase all even numbers
    for (; it != numbers.end(); ++it) {
        int n = *it;
        if (n % 2 == 0) {
            // wouldn't invalidate the iterator?
            numbers.erase(it);
        }
    }

    // print '1 3 5 7 9'
    std::for_each(numbers.begin(), numbers.end(), printElement);

    return 0;
}

İlk başta, kümeden bir öğeyi yineleyerek silmenin yineleyiciyi geçersiz kılacağını ve for döngüsündeki artışın tanımsız davranışa sahip olacağını düşündüm. Yine de bu test kodunu çalıştırdım ve her şey yolunda gitti ve nedenini açıklayamıyorum.

Sorum: Bu, standart kümeler için tanımlanmış davranış mı yoksa bu uygulamaya özel mi? Bu arada ubuntu 10.04'te (32 bit sürüm) gcc 4.3.3 kullanıyorum.

Teşekkürler!

Önerilen çözüm:

Bu, kümedeki öğeleri yinelemenin ve silmenin doğru bir yolu mu?

while(it != numbers.end()) {
    int n = *it;
    if (n % 2 == 0) {
        // post-increment operator returns a copy, then increment
        numbers.erase(it++);
    } else {
        // pre-increment operator increments, then return
        ++it;
    }
}

Düzenleme: TERCİH EDİLEN ÇÖZÜM

Tam olarak aynı şeyi yapsa da, bana daha zarif görünen bir çözüm buldum.

while(it != numbers.end()) {
    // copy the current iterator then increment it
    std::set<int>::iterator current = it++;
    int n = *current;
    if (n % 2 == 0) {
        // don't invalidate iterator it, because it is already
        // pointing to the next element
        numbers.erase(current);
    }
}

Süre içinde birkaç test koşulu varsa, her birinin yineleyiciyi artırması gerekir. Bu kodu daha çok seviyorum çünkü yineleyici yalnızca tek bir yerde artırılarak kodu daha az hataya açık ve daha okunaklı hale getiriyor.



3
Aslında, bu soruyu (ve diğerlerini) kendime sormadan önce okudum, ancak bunlar diğer STL kaplarıyla ilişkili olduklarından ve ilk testim görünüşte işe yaradığından, aralarında bazı farklar olduğunu düşündüm. Ancak Matt'in cevabından sonra valgrind kullanmayı düşündüm. Yine de YENİ çözümümü diğerlerine tercih ediyorum çünkü yineleyiciyi tek bir yerde artırarak hata olasılığını azaltıyor. Yardımınız için hepinize teşekkür ederim!
pedromanoel

1
@pedromanoel , yineleyicinin görünmez bir geçici kopyasının kullanılmasını gerektirmediğinden ++itbiraz daha verimli olmalıdır it++. Kornel'in versiyonu artık filtrelenmemiş öğelerin en verimli şekilde yinelenmesini sağlar.
Alnitak

@Alnitak Bunu düşünmedim ama performanstaki farkın o kadar da büyük olmayacağını düşünüyorum. Kopya da onun sürümünde oluşturulur, ancak yalnızca eşleşen öğeler için. Yani optimizasyon derecesi tamamen setin yapısına bağlıdır. Bir süredir kodu önceden optimize ettim, işlem sırasında okunabilirliği ve kodlama hızını bozdum ... Bu yüzden diğer yolu kullanmadan önce bazı testler yapardım.
pedromanoel

Yanıtlar:


181

Bu uygulamaya bağlıdır:

Standart 23.1.2.8:

Ekleme üyeleri yineleyicilerin geçerliliğini ve kaba referansları etkilemeyecek ve silme üyeleri yalnızca yineleyicileri ve silinen öğelere yapılan referansları geçersiz kılacaktır.

Belki bunu deneyebilirsiniz - bu standart uyumludur:

for (auto it = numbers.begin(); it != numbers.end(); ) {
    if (*it % 2 == 0) {
        numbers.erase(it++);
    }
    else {
        ++it;
    }
}

İt ++ 'nın postfix olduğunu unutmayın, bu nedenle eski konumu silmek için geçer, ancak önce operatör nedeniyle daha yenisine atlar.

2015.10.27 güncellemesi: C ++ 11 kusuru çözdü. iterator erase (const_iterator position);kaldırılan son öğeyi takip eden öğeye bir yineleyici döndürür (veya set::endson öğe kaldırılmışsa). Yani C ++ 11 stili:

for (auto it = numbers.begin(); it != numbers.end(); ) {
    if (*it % 2 == 0) {
        it = numbers.erase(it);
    }
    else {
        ++it;
    }
}

2
Bu, deque MSVC2013'te çalışmaz. Ya uygulamaları hatalı ya da bunun üzerinde çalışmasını engelleyen başka bir gereklilik var deque. STL spesifikasyonu o kadar karmaşıktır ki, sıradan programcınızın ezberlemesini bırakın, tüm uygulamaların onu takip etmesini bekleyemezsiniz. STL evcilleştirmenin ötesinde bir canavardır ve benzersiz bir uygulama olmadığından (ve eğer varsa, görünüşe göre bir döngüdeki öğeleri silmek gibi açık durumları kapsamamaktadır), bu da STL'yi yukarı çıkabilen parlak kırılgan bir oyuncak yapar. Yandan baktığınızda bir patlama.
kuroi neko

@MatthieuM. C ++ 11'de var. C ++ 17'de, şimdi yineleyici (C ++ 11'de const_iterator) alıyor.
tartaruga_casco_mole

19

Programınızı valgrind üzerinden çalıştırırsanız, bir dizi okuma hatası görürsünüz. Başka bir deyişle, evet, yineleyiciler geçersiz kılınıyor, ancak örneğinizde şanslısınız (ya da tanımlanmamış davranışların olumsuz etkilerini görmediğiniz için gerçekten şanssızsınız). Bunun bir çözümü, geçici bir yineleyici oluşturmak, sıcaklığı artırmak, hedef yineleyiciyi silmek ve ardından hedefi geçici olarak ayarlamaktır. Örneğin, döngünüzü aşağıdaki gibi yeniden yazın:

std::set<int>::iterator it = numbers.begin();                               
std::set<int>::iterator tmp;                                                

// iterate through the set and erase all even numbers                       
for ( ; it != numbers.end(); )                                              
{                                                                           
    int n = *it;                                                            
    if (n % 2 == 0)                                                         
    {                                                                       
        tmp = it;                                                           
        ++tmp;                                                              
        numbers.erase(it);                                                  
        it = tmp;                                                           
    }                                                                       
    else                                                                    
    {                                                                       
        ++it;                                                               
    }                                                                       
} 

Önemli olan ve kapsam içi başlatma veya işlem sonrası gerektirmeyen tek koşulsa, whiledöngüyü kullanmak daha iyidir . ie for ( ; it != numbers.end(); )ile daha iyi görünürwhile (it != numbers.end())
iammilind

7

"Tanımlanmamış davranış" ın ne anlama geldiğini yanlış anlıyorsunuz. Tanımsız davranış "bunu yaparsanız, programınız anlamına gelmez olacaktır çökmesine veya beklenmeyen sonuçlar doğurur." Bu "bunu yaparsanız, programınız demektir olabilir çökmesine veya beklenmeyen sonuçlar üretmek" ya vb derleyici, işletim sisteminin, ayın fazı, bağlı başka bir şey yapmak

Bir şey çökmeden çalışırsa ve beklediğiniz gibi davranırsa, bu onun tanımsız bir davranış olmadığının kanıtı değildir. Tek kanıtladığı, davranışının, söz konusu işletim sisteminde söz konusu derleyici ile derlendikten sonra söz konusu belirli çalıştırma için gözlemlendiği gibi olduğudur.

Bir kümeden bir öğeyi silmek, silinen öğeye giden yineleyiciyi geçersiz kılar. Geçersiz bir yineleyici kullanmak tanımsız bir davranıştır. Tam da öyle oldu ki, gözlemlenen davranış bu belirli durumda amaçladığınız şeydi; kodun doğru olduğu anlamına gelmez.


Oh, tanımlanmamış davranışların aynı zamanda "Benim için işe yarıyor, ama herkes için değil" anlamına da gelebileceğinin farkındayım. Bu yüzden bu soruyu sordum çünkü bu davranışın doğru olup olmadığını bilmiyordum. Öyle olsaydı, öylece ayrılırdım. Bir while döngüsü kullanmak sorunumu çözer mi? Sorumu önerilen çözümümle düzenledim. Lütfen kontrol et.
pedromanoel

Benim için de çalışıyor. Ancak koşulu şu şekilde değiştirdiğimde if (n > 2 && n < 7 )0 1 2 4 7 8 9 elde ederim . - Buradaki belirli sonuç muhtemelen ayın evresinden ziyade silme yönteminin uygulama ayrıntılarına ve ayar yineleyicilerine bağlıdır (bu değil uygulama ayrıntılarına güvenmelidir). ;)
UncleBens

1
STL, "tanımlanmamış davranışa" birçok yeni anlam katar. Örneğin, "Microsoft std::set::erase, bir yineleyici döndürmeye izin vererek spesifikasyonu geliştirmeyi akıllıca düşündü , böylece MSVC kodunuz gcc tarafından derlendiğinde bir patlama ile yükselecek" veya "Microsoft, std::bitset::operator[]dikkatle optimize edilmiş bit seti algoritmanızın MSVC ile derlendiğinde tarama ". STL hiçbir benzersiz bir uygulama vardır ve bir döngü içinden elemanları silme şaşılacak kıdemli programcı uzmanlık gerektiren bu yüzden onun Spec ..., katlanarak büyüyen şişirilmiş karmaşa
kuroi neko

2

Bir deque konteyneri durumunda, deque yineleyicinin numbers.end () 'e eşitliğini kontrol eden tüm çözümlerin gcc 4.8.4'te başarısız olacağı konusunda uyarmak gerekirse. Yani, deque'in bir elemanının silinmesi, genellikle sayıların göstericisini geçersiz kılar. End ():

#include <iostream>
#include <deque>

using namespace std;
int main() 
{

  deque<int> numbers;

  numbers.push_back(0);
  numbers.push_back(1);
  numbers.push_back(2);
  numbers.push_back(3);
  //numbers.push_back(4);

  deque<int>::iterator  it_end = numbers.end();

  for (deque<int>::iterator it = numbers.begin(); it != numbers.end(); ) {
    if (*it % 2 == 0) {
      cout << "Erasing element: " << *it << "\n";
      numbers.erase(it++);
      if (it_end == numbers.end()) {
    cout << "it_end is still pointing to numbers.end()\n";
      } else {
    cout << "it_end is not anymore pointing to numbers.end()\n";
      }
    }
    else {
      cout << "Skipping element: " << *it << "\n";
      ++it;
    }
  }
}

Çıktı:

Erasing element: 0
it_end is still pointing to numbers.end()
Skipping element: 1
Erasing element: 2
it_end is not anymore pointing to numbers.end()

Bu özel durumda deque dönüşümü doğru olsa da, son göstericinin yol boyunca geçersiz kılındığına dikkat edin. Farklı bir boyutta deque ile hata daha belirgindir:

int main() 
{

  deque<int> numbers;

  numbers.push_back(0);
  numbers.push_back(1);
  numbers.push_back(2);
  numbers.push_back(3);
  numbers.push_back(4);

  deque<int>::iterator  it_end = numbers.end();

  for (deque<int>::iterator it = numbers.begin(); it != numbers.end(); ) {
    if (*it % 2 == 0) {
      cout << "Erasing element: " << *it << "\n";
      numbers.erase(it++);
      if (it_end == numbers.end()) {
    cout << "it_end is still pointing to numbers.end()\n";
      } else {
    cout << "it_end is not anymore pointing to numbers.end()\n";
      }
    }
    else {
      cout << "Skipping element: " << *it << "\n";
      ++it;
    }
  }
}

Çıktı:

Erasing element: 0
it_end is still pointing to numbers.end()
Skipping element: 1
Erasing element: 2
it_end is still pointing to numbers.end()
Skipping element: 3
Erasing element: 4
it_end is not anymore pointing to numbers.end()
Erasing element: 0
it_end is not anymore pointing to numbers.end()
Erasing element: 0
it_end is not anymore pointing to numbers.end()
...
Segmentation fault (core dumped)

İşte bunu düzeltmenin yollarından biri:

#include <iostream>
#include <deque>

using namespace std;
int main() 
{

  deque<int> numbers;
  bool done_iterating = false;

  numbers.push_back(0);
  numbers.push_back(1);
  numbers.push_back(2);
  numbers.push_back(3);
  numbers.push_back(4);

  if (!numbers.empty()) {
    deque<int>::iterator it = numbers.begin();
    while (!done_iterating) {
      if (it + 1 == numbers.end()) {
    done_iterating = true;
      } 
      if (*it % 2 == 0) {
    cout << "Erasing element: " << *it << "\n";
      numbers.erase(it++);
      }
      else {
    cout << "Skipping element: " << *it << "\n";
    ++it;
      }
    }
  }
}

Anahtar varlık do not trust an old remembered dq.end() value, always compare to a new call to dq.end().
Jesse Chisholm

2

C ++ 20, "tek tip kapsayıcı silme" özelliğine sahip olacak ve şunları yazabileceksiniz:

std::erase_if(numbers, [](int n){ return n % 2 == 0 });

Ve bunun için çalışacak vector, set, dequevb bkz cppReference fazla bilgi için.


1

Bu davranış, uygulamaya özeldir. Yineleyicinin doğruluğunu garanti etmek için "it = numbers.erase (it);" öğeyi silmeniz ve diğer durumda basitçe yinelemeyi teşvik etmeniz gerekiyorsa ifade.


1
Set<T>::erasesürüm yineleyici döndürmez.
Arkaitz Jimenez

4
Aslında öyle, ancak yalnızca MSVC uygulamasında. Yani bu gerçekten uygulamaya özel bir cevaptır. :)
Eugene

1
@Eugene C ++ 11 ile tüm uygulamalar için yapar
mastov

Bazı uygulama gcc 4.8ile c++1ysilme bir hata var. it = collection.erase(it);çalışması gerekiyor, ancak kullanımı daha güvenli olabilircollection.erase(it++);
Jesse Chisholm

1

Bence ' remove_if' STL yöntemini kullanmanın , yineleyici tarafından sarılmış nesneyi silmeye çalışırken bazı garip sorunları önlemeye yardımcı olabileceğini düşünüyorum .

Bu çözüm daha az verimli olabilir.

Diyelim ki vektör gibi bir tür konteynırımız veya m_bullet adlı bir liste var:

Bullet::Ptr is a shared_pr<Bullet>

' it' remove_ifdöndüren yineleyicidir , üçüncü argüman, kabın her elemanı üzerinde çalıştırılan bir lambda işlevidir. Kap içerdiği için Bullet::Ptr, lambda işlevinin bu türü (veya bu türe bir başvuruyu) bağımsız değişken olarak geçirmesi gerekir.

 auto it = std::remove_if(m_bullets.begin(), m_bullets.end(), [](Bullet::Ptr bullet){
    // dead bullets need to be removed from the container
    if (!bullet->isAlive()) {
        // lambda function returns true, thus this element is 'removed'
        return true;
    }
    else{
        // in the other case, that the bullet is still alive and we can do
        // stuff with it, like rendering and what not.
        bullet->render(); // while checking, we do render work at the same time
        // then we could either do another check or directly say that we don't
        // want the bullet to be removed.
        return false;
    }
});
// The interesting part is, that all of those objects were not really
// completely removed, as the space of the deleted objects does still 
// exist and needs to be removed if you do not want to manually fill it later 
// on with any other objects.
// erase dead bullets
m_bullets.erase(it, m_bullets.end());

' remove_if', lambda işlevinin true döndürdüğü kabı kaldırır ve bu içeriği kabın başlangıcına kaydırır. ' it', Çöp olarak kabul edilebilecek tanımlanmamış bir nesneye işaret eder. 'İt' ile m_bullets.end () arasındaki nesneler hafızayı kapladıkları için ancak çöp içerdikleri için silinebilirler, bu nedenle bu aralıkta 'silme' yöntemi çağrılır.


0

Aynı eski sorunla karşılaştım ve aşağıdaki kodu daha anlaşılır buldum , bu da yukarıdaki çözümlere göre bir şekilde.

std::set<int*>::iterator beginIt = listOfInts.begin();
while(beginIt != listOfInts.end())
{
    // Use your member
    std::cout<<(*beginIt)<<std::endl;

    // delete the object
    delete (*beginIt);

    // erase item from vector
    listOfInts.erase(beginIt );

    // re-calculate the begin
    beginIt = listOfInts.begin();
}

Bu sadece her öğeyi her zaman silecekseniz işe yarar . OP, öğeleri seçici olarak silmek ve hala geçerli yineleyiciler bulundurmakla ilgilidir.
Jesse Chisholm
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.