C ++ 11 'for' döngüsünde iken vektörden öğe kaldırılıyor mu?


100

Bir IInventory * vektörüne sahibim ve her biriyle bir şeyler yapmak için C ++ 11 aralığını kullanarak listede dönüyorum.

Biriyle bazı şeyler yaptıktan sonra, onu listeden çıkarmak ve nesneyi silmek isteyebilirim. deleteİşaretçiyi temizlemek için istediğim zaman çağırabileceğimi biliyorum , ancak aralık fordöngüsündeyken onu vektörden kaldırmanın doğru yolu nedir? Ve onu listeden çıkarırsam döngüm geçersiz olur mu?

std::vector<IInventory*> inv;
inv.push_back(new Foo());
inv.push_back(new Bar());

for (IInventory* index : inv)
{
    // Do some stuff
    // OK, I decided I need to remove this object from 'inv'...
}

8
Süslü olmak istiyorsanız std::remove_if, "işler yapan" ve öğenin kaldırılmasını istiyorsanız doğru döndüren bir yüklem ile kullanabilirsiniz .
Benjamin Lindley

Sadece bir indeks sayacı ekleyip inv.erase (indeks) gibi bir şey kullanmamanın bir nedeni var mı?
TomJ

4
@TomJ: Bu yinelemeyi yine de mahveder.
Ben Voigt

@TomJ ve bu bir performans katili olurdu - her silme işleminde, silindikten sonra tüm öğeleri hareket ettirirsiniz.
Ivan

1
@BenVoigt ben geçiş tavsiye std::listaltında
bobobobo

Yanıtlar:


99

Hayır, yapamazsınız. Menzile dayalı for, bir kabın her öğesine bir kez erişmeniz gerektiğinde kullanılır.

forKabı ilerledikçe değiştirmeniz, bir öğeye birden fazla kez erişmeniz veya kapsayıcı aracılığıyla doğrusal olmayan bir şekilde yineleme yapmanız gerekirse , normal döngüyü veya kuzenlerinden birini kullanmalısınız .

Örneğin:

auto i = std::begin(inv);

while (i != std::end(inv)) {
    // Do some stuff
    if (blah)
        i = inv.erase(i);
    else
        ++i;
}

5
Burada geçerli olan deyimi sil-kaldır olmaz mı?
Naveen

4
@Naveen Bunu yapmamaya karar verdim çünkü görünüşe göre her öğeyi yinelemesi, onunla hesaplamalar yapması ve ardından muhtemelen onu kaptan çıkarması gerekiyor. Silme-kaldırma, yalnızca bir yüklemin döndüğü öğeleri ( trueAFAIU) sildiğinizi söylüyor ve yineleme mantığını yüklemle karıştırmamak bu şekilde daha iyi görünüyor.
Seth Carnegie

4
@SethCarnegie Tahmin için bir lambda ile silme-kaldırma zarif bir şekilde buna izin verir (çünkü bu zaten C ++ 11'dir)
Potatoswatter

11
Bu çözümü beğenmedim, çoğu kap için O (N ^ 2). remove_ifdaha iyi.
Ben Voigt

6
Bu cevap olduğunu , doğru erasegeçerli yeni bir yineleyici döndürür. verimli olmayabilir, ancak çalışması garantilidir.
sp2danny

56

Bir öğe vektörden her kaldırıldığında, silinen öğenin ardından gelen öğelerin her biri taşındığından, silinen öğedeki veya sonraki yineleyicilerin artık geçerli olmadığını varsaymalısınız.

Aralık tabanlı bir for-döngüsü, yineleyiciler kullanan "normal" döngü için sözdizimsel şekerdir, bu nedenle yukarıdakiler geçerlidir.

Bununla birlikte, basitçe şunları yapabilirsiniz:

inv.erase(
    std::remove_if(
        inv.begin(),
        inv.end(),
        [](IInventory* element) -> bool {
            // Do "some stuff", then return true if element should be removed.
            return true;
        }
    ),
    inv.end()
);

6
" çünkü vektörün, öğelerini sakladığı bellek bloğunu yeniden tahsis etme olasılığı vardır " Hayır, a vectorbir çağrı nedeniyle asla yeniden ayrılmayacaktır erase. Yineleyicilerin geçersiz kılınmasının nedeni, silinen öğeden sonraki öğelerin her birinin taşınmasıdır.
ildjarn

2
[&]Yerel değişkenlerle "bazı şeyler yapmasına" izin vermek için bir yakalama varsayılanı uygun olacaktır.
Potatoswatter

2
Bu ek olarak sizin surround hatırlamak zorunda, bir yineleyici tabanlı döngü daha basit görünmüyor remove_ifile .eraseaksi hiçbir şey olmaz.
bobobobo

3
@bobobobo "Yineleyici tabanlı döngü" ile Seth Carnegie'nin cevabını kastediyorsanız , bu ortalama olarak O (n ^ 2). std::remove_ifO (n).
Branko Dimitrijevic

1
@bobobobo Aslında rastgele erişime ihtiyacınız olmadığı sürece .
Branko Dimitrijevic

16

İdeal olarak, üzerinde yinelerken vektörü değiştirmemelisiniz. Sil-kaldır deyimini kullanın. Eğer yaparsanız, birkaç sorunla karşılaşmanız olasıdır. Bir de yana vectorbir erasegeçersiz tüm yineleyiciler elemanı ile başlayan kadar silinmeye end()Eğer kullanarak yineleyiciler geçerli kalmasını sağlamak gerekir:

for (MyVector::iterator b = v.begin(); b != v.end();) { 
    if (foo) {
       b = v.erase( b ); // reseat iterator to a valid value post-erase
    else {
       ++b;
    }
}

b != v.end()Teste olduğu gibi ihtiyacınız olduğunu unutmayın . Aşağıdaki gibi optimize etmeye çalışırsanız:

for (MyVector::iterator b = v.begin(), e = v.end(); b != e;)

eİlk erasearamadan sonra geçersiz kılındığınız için UB ile karşılaşacaksınız .


@ildjarn: Evet, doğru! Bu bir yazım hatasıydı.
dirkgently

2
Bu sil-kaldır deyimi değildir. Çağrı yok std::removeve O (N ^ 2) değil O (N).
Potatoswatter

1
@Potatoswatter: Elbette hayır. Yinelediğinizde silme ile ilgili sorunlara işaret etmeye çalışıyordum. Sanırım ifadelerim geçmedi mi?
Dirkgently

5

Bu döngüde öğeleri kaldırmak kesin bir gereklilik mi? Aksi takdirde, silmek istediğiniz işaretçileri NULL olarak ayarlayabilir ve tüm NULL işaretçileri kaldırmak için vektör üzerinden başka bir geçiş yapabilirsiniz.

std::vector<IInventory*> inv;
inv.push_back( new Foo() );
inv.push_back( new Bar() );

for ( IInventory* &index : inv )
{
    // do some stuff
    // ok I decided I need to remove this object from inv...?
    if (do_delete_index)
    {
        delete index;
        index = NULL;
    }
}
std::remove(inv.begin(), inv.end(), NULL);

2

necroposting için özür dilerim ve ayrıca c ++ uzmanlığım cevabımın önüne geçerse özür dilerim, ancak her öğeyi yinelemeye ve olası değişiklikleri yapmaya (bir dizini silmek gibi) çalışıyorsanız, döngü için bir backwords kullanmayı deneyin.

for(int x=vector.getsize(); x>0; x--){

//do stuff
//erase index x

}

x dizinini silerken, sonraki döngü son yinelemenin "önündeki" öğe için olacaktır. Umarım bu birine yardım eder


sadece belirli bir dizine erişmek için x'i kullanırken, x-1 lol yapın
lilbigwill99

1

Tamam, ben yine geç kaldım ama: Üzgünüm, doğru ana kadar okuduklarım - bu ise mümkün, sadece iki yineleyiciler gerekir:

std::vector<IInventory*>::iterator current = inv.begin();
for (IInventory* index : inv)
{
    if(/* ... */)
    {
        delete index;
    }
    else
    {
        *current++ = index;
    }
}
inv.erase(current, inv.end());

Sadece bir yineleyicinin işaret ettiği değeri değiştirmek, başka hiçbir yineleyiciyi geçersiz kılmaz, bu yüzden bunu endişelenmeden yapabiliriz. Aslında, std::remove_if(en azından gcc uygulaması) çok benzer bir şey yapar (klasik bir döngü kullanarak ...), sadece hiçbir şeyi silmez ve silmez.

Bununla birlikte, bunun iş parçacığı açısından güvenli olmadığını unutmayın (!) - ancak bu, yukarıdaki diğer çözümlerin bazıları için de geçerlidir ...


Ne halt. Bu bir abartıdır.
Kesse

@Kesse Gerçekten mi? Bu, vektörlerle elde edebileceğiniz en verimli algoritmadır (aralık tabanlı döngü veya klasik yineleme döngüsü farketmez): Bu şekilde, vektördeki her bir öğeyi en fazla bir kez hareket ettirirsiniz ve tüm vektör üzerinde tam olarak bir kez yinelersiniz. Eşleşen her bir öğeyi erasesilerseniz (tabii ki birden fazla öğeyi silmeniz koşuluyla) , sonraki öğeleri kaç kez taşır ve vektör üzerinde yinelersiniz ?
Aconcagua

1

Örnekle göstereceğim, aşağıdaki örnek vektörden tuhaf öğeleri kaldırır:

void test_del_vector(){
    std::vector<int> vecInt{0, 1, 2, 3, 4, 5};

    //method 1
    for(auto it = vecInt.begin();it != vecInt.end();){
        if(*it % 2){// remove all the odds
            it = vecInt.erase(it);
        } else{
            ++it;
        }
    }

    // output all the remaining elements
    for(auto const& it:vecInt)std::cout<<it;
    std::cout<<std::endl;

    // recreate vecInt, and use method 2
    vecInt = {0, 1, 2, 3, 4, 5};
    //method 2
    for(auto it=std::begin(vecInt);it!=std::end(vecInt);){
        if (*it % 2){
            it = vecInt.erase(it);
        }else{
            ++it;
        }
    }

    // output all the remaining elements
    for(auto const& it:vecInt)std::cout<<it;
    std::cout<<std::endl;

    // recreate vecInt, and use method 3
    vecInt = {0, 1, 2, 3, 4, 5};
    //method 3
    vecInt.erase(std::remove_if(vecInt.begin(), vecInt.end(),
                 [](const int a){return a % 2;}),
                 vecInt.end());

    // output all the remaining elements
    for(auto const& it:vecInt)std::cout<<it;
    std::cout<<std::endl;

}

aşağıdaki aw çıkışı:

024
024
024

Unutmayın, yöntem erase, geçen yineleyicinin bir sonraki yineleyicisini döndürecektir.

Dan burada , biz daha oluşturmak yöntemini kullanabilirsiniz:

template<class Container, class F>
void erase_where(Container& c, F&& f)
{
    c.erase(std::remove_if(c.begin(), c.end(),std::forward<F>(f)),
            c.end());
}

void test_del_vector(){
    std::vector<int> vecInt{0, 1, 2, 3, 4, 5};
    //method 4
    auto is_odd = [](int x){return x % 2;};
    erase_where(vecInt, is_odd);

    // output all the remaining elements
    for(auto const& it:vecInt)std::cout<<it;
    std::cout<<std::endl;    
}

Nasıl kullanılacağını görmek için buraya bakın std::remove_if. https://en.cppreference.com/w/cpp/algorithm/remove


1

Bu konu başlığının aksine, iki geçiş kullanırım:

#include <algorithm>
#include <vector>

std::vector<IInventory*> inv;
inv.push_back(new Foo());
inv.push_back(new Bar());

std::vector<IInventory*> toDelete;

for (IInventory* index : inv)
{
    // Do some stuff
    if (deleteConditionTrue)
    {
        toDelete.push_back(index);
    }
}

for (IInventory* index : toDelete)
{
    inv.erase(std::remove(inv.begin(), inv.end(), index), inv.end());
}

0

Çok daha zarif bir çözüm, geçiş yapmak olacaktır std::list(hızlı rastgele erişime ihtiyacınız olmadığını varsayarak).

list<Widget*> widgets ; // create and use this..

Daha sonra .remove_iftek satırda bir C ++ functor ile silebilirsiniz :

widgets.remove_if( []( Widget*w ){ return w->isExpired() ; } ) ;

Yani burada sadece bir argümanı (the Widget*) kabul eden bir functor yazıyorum . Dönüş değeri, a'nın Widget*listeden kaldırılacağı koşuldur .

Bu sözdizimini lezzetli buluyorum. Ben hiç kullanmak sanmıyorum remove_ifiçin std :: vektörler - orada çok olduğunu inv.begin()ve inv.end()gürültü kullandığınız muhtemelen daha çok işinize bir tamsayı-dizin tabanlı silme ya da sadece düz normal eski yineleyici tabanlı gösterildiği gibi (silme altında). Ancak std::vectoryine de ortasından çok fazla uzaklaşmamalısınız , bu nedenle listbu durum için listenin ortasının sık sık silinmesi için a'ya geçilmesi önerilir.

Ancak , kaldırılanları arama şansım olmadığına deletedikkat edin Widget*. Bunu yapmak için şöyle görünecektir:

widgets.remove_if( []( Widget*w ){
  bool exp = w->isExpired() ;
  if( exp )  delete w ;       // delete the widget if it was expired
  return exp ;                // remove from widgets list if it was expired
} ) ;

Bunun gibi düzenli yineleyici tabanlı bir döngü de kullanabilirsiniz:

//                                                              NO INCREMENT v
for( list<Widget*>::iterator iter = widgets.begin() ; iter != widgets.end() ; )
{
  if( (*iter)->isExpired() )
  {
    delete( *iter ) ;
    iter = widgets.erase( iter ) ; // _advances_ iter, so this loop is not infinite
  }
  else
    ++iter ;
}

Uzunluğunu beğenmediyseniz for( list<Widget*>::iterator iter = widgets.begin() ; ...kullanabilirsiniz

for( auto iter = widgets.begin() ; ...

1
Nasıl hoşuna anladığını sanmıyorum remove_ifbir on std::vectoraslında işin ve O (N) karmaşıklık tutar nasıl.
Ben Voigt

Bu önemli değil. A'nın ortasından std::vectorçıkarmak, her bir öğeyi birinden sonra çıkardıktan sonra kaydıracak ve bu std::listda çok daha iyi bir seçim olacaktır.
bobobobo

1
Hayır, "her öğeyi bir yukarı kaydırmaz". remove_ifher bir öğeyi serbest bırakılan boşluk sayısına göre yukarı kaydırır. Zamanla, önbellek kullanım hesaplaması remove_ifbir üzerinde std::vectorbir gelen muhtemel Mağazasından çıkarılması std::list. Ve O(1)rastgele erişimi korur .
Ben Voigt

1
O halde bir soru ararken harika bir cevabınız var. Bu soru , her iki kap için O (N) olan listeyi yinelemekten bahsediyor. Ve her iki kap için de O (N) olan O (N) elemanlarının kaldırılması.
Ben Voigt

2
Ön işaretleme gerekli değildir; bunu tek geçişte yapmak tamamen mümkün. Sadece "incelenecek bir sonraki öğe" ve "doldurulacak bir sonraki boşluk" kaydını tutmanız gerekir. Bunu, yüklemeye göre filtrelenmiş listenin bir kopyasını oluşturmak olarak düşünün. Yüklem doğru dönerse, öğeyi atlayın, aksi takdirde kopyalayın. Ancak liste kopyası yerinde yapılır ve kopyalama yerine takas / taşıma kullanılır.
Ben Voigt

0

Sanırım şunu yapardım ...

for (auto itr = inv.begin(); itr != inv.end();)
{
   // Do some stuff
   if (OK, I decided I need to remove this object from 'inv')
      itr = inv.erase(itr);
   else
      ++itr;
}

0

döngü yinelemesi sırasında yineleyiciyi silemezsiniz çünkü yineleyici sayısı uyumsuz olur ve bazı yinelemelerden sonra geçersiz yineleyiciniz olur.

Çözüm: 1) orijinal vektörün kopyasını alın 2) bu kopyayı kullanarak yineleyiciyi yineleyin 2) bazı şeyler yapın ve orijinal vektörden silin.

std::vector<IInventory*> inv;
inv.push_back(new Foo());
inv.push_back(new Bar());

std::vector<IInventory*> copyinv = inv;
iteratorCout = 0;
for (IInventory* index : copyinv)
{
    // Do some stuff
    // OK, I decided I need to remove this object from 'inv'...
    inv.erase(inv.begin() + iteratorCout);
    iteratorCout++;
}  

0

Öğeyi tek tek silmek, kolayca N ^ 2 performansına yol açar. Silinmesi gereken öğeleri işaretlemek ve döngüden sonra hemen silmek daha iyidir. Vektörünüzde nullptr'nin geçerli olmadığını varsayabilirsem, o zaman

std::vector<IInventory*> inv;
// ... push some elements to inv
for (IInventory*& index : inv)
{
    // Do some stuff
    // OK, I decided I need to remove this object from 'inv'...
    {
      delete index;
      index =nullptr;
    }
}
inv.erase( std::remove( begin( inv ), end( inv ), nullptr ), end( inv ) ); 

çalışmalı.

"Bir şeyler yapın", vektörün öğelerini değiştirmiyorsa ve yalnızca öğeyi kaldırmak veya tutmak için karar vermek için kullanılıyorsa, onu lambda'ya dönüştürebilir (birinin önceki gönderisinde önerildiği gibi) ve

inv.erase( std::remove_if( begin( inv ), end( inv ), []( Inventory* i )
  {
    // DO some stuff
    return OK, I decided I need to remove this object from 'inv'...
  } ), end( inv ) );
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.