2012-04-28 282 views
70

我有一個IInventory *的向量,我正在循環使用C++ 11範圍的列表,以便與每一個做相關的事情。從C++ 11範圍'for'循環中移除vector中的項目?

做了一些東西后,我可能想從列表中刪除它並刪除該對象。我知道我可以在指針上隨時撥打delete來清除它,但是在範圍for循環中,從矢量中刪除它的正確方法是什麼?如果我從列表中刪除它,我的循環會失效嗎?

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'... 
} 
+7

如果你想獲得幻想,你可以使用'與謂詞「做的東西」的std :: remove_if'然後如果你想刪除該元素則返回true。 – 2012-04-28 04:06:11

+0

是否有一個原因,你不能只是添加一個索引計數器,然後使用像inv.erase(索引)這樣的東西? – TomJ 2012-04-28 04:06:33

+3

@TomJ:這仍然會影響迭代。 – 2012-04-28 04:11:39

回答

63

不,你不能。基於範圍的for適用於需要訪問容器的每個元素一次。

如果您需要隨時修改容器,多次訪問元素或以非線性方式遍歷容器,則應使用正常的for循環或其一個表親。

例如:

auto i = std::begin(inv); 

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

不會擦除 - 刪除適用於這裏的習語嗎? – Naveen 2012-04-28 04:06:23

+3

@Naveen我決定不要這麼做,因爲顯然他需要遍歷每個項目,對它進行計算,然後_possibly_將它從容器中移除。擦除刪除說,你只是擦除謂詞返回「真」的元素,AFAIU,並且這樣看起來更好,這樣不會將迭代邏輯與謂詞混合在一起。 – 2012-04-28 04:07:50

+3

@SethCarnegie擦除 - 刪除與謂詞優雅的lambda允許(因爲這已經是C++ 11) – Potatoswatter 2012-04-28 23:13:41

9

在遍歷它你最好不要修改載體。使用擦除刪除成語。如果你這樣做,你可能會遇到一些問題。由於在vectorerase無效與元素開頭的所有迭代器被擦除高達的end()你將需要使用,以確保您的迭代器仍然有效:

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()測試原樣。如果你試圖優化它,如下所示:

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

你會遇到UB因爲你e是第一erase調用後失效。

+0

@ildjarn:是的,真的!這是一個錯字。 – dirkgently 2012-04-28 04:40:42

+1

這不是刪除成語。沒有調用'std :: remove',它是O(N^2)而不是O(N)。 – Potatoswatter 2012-04-28 23:15:55

+0

@Patatoswatter:當然不是。我試圖指出在迭代時刪除的問題。我猜我的措辭沒有通過召集? – dirkgently 2012-04-29 03:34:02

40

每次從向量中刪除一個元素時,都必須假定迭代器在擦除元素之後或之後不再有效,因爲擦除元素之後的每個元素都將被移動。

基於範圍的for循環僅僅是使用迭代器的「正常」循環的語法糖,所以上面適用。

話雖這麼說,你可以簡單地說:

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() 
); 
+3

*「,因爲有可能是向量重新分配了保留其元素的內存塊*」不,由於調用「擦除」,「向量」永遠不會重新分配。迭代器失效的原因是因爲擦除元素之後的每個元素都被移動。 – ildjarn 2012-04-28 04:40:15

+0

@ildjarn我站好了,先生! – 2012-04-28 05:00:08

+1

「[&]」的捕獲默認值是合適的,以允許他用局部變量「做一些事情」。 – Potatoswatter 2012-04-28 23:17:58

5

它是一個嚴格的要求,在循環中移除元素,同時?否則,您可以將要刪除的指針設置爲NULL,並對該向量進行另一次傳遞以刪除所有NULL指針。

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

一個更優雅的解決辦法是切換到std::list(假設你並不需要快速隨機訪問)。

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

你可以接着用.remove_if和某行的C++函數對象刪除:

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

所以在這裏,我只是寫一個接受一個參數(Widget*)仿函數。返回值是從列表中刪除Widget*的條件。

我覺得這個語法可口。我不認爲我會使用remove_ifstd::vectors - 有這麼多inv.begin()inv.end()噪聲那裏你可能會更好使用an integer-index-based delete或只是一個普通的舊的常規基於迭代器的刪除(如下所示)。但是,您不應該真的從std::vector的中間刪除,因此建議您切換到list以處理這種頻繁中途刪除的情況。

但請注意,我沒有機會致電Widget*delete已刪除。要做到這一點,就應該是這樣的:

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

你也可以使用常規的基於迭代器的循環,像這樣:

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

如果你不喜歡的for(list<Widget*>::iterator iter = widgets.begin() ; ...的長度,你可以使用

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

我不認爲你明白'std :: vector'上的'remove_if'實際上是如何工作的,以及它如何保持O(N)的複雜性。 – 2013-04-30 19:16:57

+0

這沒關係。從'std :: vector'的中間去除總是會將每個元素放在一個元素之後,使得'std :: list'成爲更好的選擇。 – bobobobo 2013-04-30 19:57:16

+1

不,它不會「滑動每個元素」。 'remove_if'會將每個元素向上滑動釋放的空間數量。在你使用緩存的時候,'std :: vector'上的'remove_if'可能比'std :: list'好。並保留'O(1)'隨機訪問。 – 2013-04-30 20:00:45

0

OK,我來晚了,但不管怎麼說:對不起,不正確的是我到目前爲止閱讀 - 這是可能的,你只需要兩個迭代器:

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

只是修改迭代器指向的值不會使任何其他迭代器失效,所以我們可以做到這一點而不必擔心。實際上,:: std :: remove_if(至少gcc實現)做了非常相似的事情(使用經典循環...),只是不刪除任何東西而且不會擦除。

注意,但是,這不是線程安全的(!) - 但是,這個應用,也爲上述一些其他的解決方案......

+0

到底是什麼。這是一個矯枉過正的問題。 – Kesse 2017-11-10 15:50:06

+0

@Kesse真的嗎?這是您可以使用矢量獲得的最有效的算法(如果基於範圍的循環或經典迭代器循環,則無關緊要):這樣,您最多隻移動一次矢量中的每個元素,並且您只對整個矢量迭代一次。如果你通過'erase'刪除了每個匹配元素(當然,你刪除了多個單個元素),你會移動後續元素並遍歷向量多少次? – Aconcagua 2017-11-12 23:23:25

0

對不起necroposting,也對不起,如果我的C++專業知識得到我的答案的方式,但如果您嘗試遍歷每個項目並進行可能的更改(如擦除索引),請嘗試使用backwords for循環。

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

//do stuff 
//erase index x 

} 

當擦除索引x時,下一個循環將用於最後一次迭代前面的項目。我真的希望這有助於有人

+0

只是不要忘記,當使用x訪問某個索引,做x-1大聲笑 – lilbigwill99 2017-05-25 20:27:21

0

我想我會做以下...

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