對於以下代碼:
my_container.erase(iter);
其中my_container是STL的某種容器,iter是指向這個容器中某個元素的迭代器。如果不是在for,while迴圈中,
這種方式刪除元素沒有問題,如果是在for,while中對m_container迭代,刪除其中合格所有元素,就可能出現問題。
如果是在for,while中對m_container迭代,刪除其中合格所有元素,就可能出現問題。
問題是:
在迭代容器的時候刪除元素,可能導致迭代器失效(invalidation of iterators),產生未定義行為
(undefined behavior);
例如,對某個迭代器解引用所獲得的值並不是執行erase()前這個迭代器指向的值,還有可能對未指向任何
元素的迭代器的解引用賦值而引發程式crash。
類似的問題代碼像這樣:
std::vector<int> my_container; for (int i = 0; i < 100; ++i) { my_container.push_back(i); } std::vector<int>::iterator it = my_container.begin(); for (it != my_container.end(); it++) { if (*it % 2 == 1) { my_container.erase(it); } }
my_container.erase(it)之後,it及其後面的迭代器已經失效,不應該再使用這些迭代器。再執行it++,其行為是未定義的。
其他容器也會遇到迭代器失效的問題:
對於vector 被刪除元素的迭代器以及指向後面元素的迭代器全部失效。
對於deque 在首部或尾部刪除元素則只會使指向被刪除元素的迭代器失效,任何其他位置的插入和刪除操作將使指向該容器元素的
所有迭代器失效。
對於list 僅有指向被刪除元素的迭代器失效。
對於(mulit)map ,(multi)set 僅有指向被刪除元素的迭代器失效。
所以Golden Rule是:盡量不要使用容器的插入刪除操作之前的迭代器。
為什麼不同容器迭代器失效情況有差別?這與實現各容器的資料結構有關。
如何在迭代容器時刪除其中的元素?各容器通用的做法如下:
std::vector<int>::iterator it = my_container.begin(); for (it != my_container.end();/**blank*/ ) { if (*it % 2 == 1) { my_container.erase(it++); } else{ it++; } }
my_container.erase(it++) 巧妙得在執行erase()之前,it 先自增,指向被刪除元素後面的元素,而給erase()傳遞的是未自增的it迭代器,
以定位要刪除的元素。如果元素的值為奇數,則刪除此元素,it指向下一個元素,如果元素的值為偶數,則檢查下一個元素的值。整個迭代過程中
迭代器就不會失效了。
上段代碼中兩個不同分支出現了i++操作,下面的程式碼範例顯示了如何防止遺忘其中任何一個分支的i++操作。
MyContainer::iterator it = myContainer.begin(); While(it != myContainer.end()){ MyContainer::iterator curIt = it; if (*curIt == matchingValue) { myContainer.erase(curIt); } }
對於vector ,deque, list, 另一種可行的方式是:
std::vector<int>::iterator it = my_container.begin(); for (it != my_container.end();/**blank*/ ) { if (*it % 2 == 1) { it = my_container.erase(it); } else{ it++; } }
上面代碼可行的原因是vector::erase() 返回一個新的迭代器,指向被刪除元素的後面的元素。可以繼續使用新的迭代器。
而出於某種未知的原因(multi)map::erase(), (multi)set::erase()沒有返回這樣的迭代器。(從C++11開始也支援返回迭代器了).
但是對於vector,諸如在0到99個數中刪除所有奇數的問題,可以使用STL的remove(),remove_if()最佳化效能。代碼如下:
bool isOdd(int value) { return (value % 2) == 1; } my_container.erase( std::remove_if(m_container.begin(), m_container.end(), isOdd), m_container.end());
讓我們再看看不使用remove_if()的版本:
for (it != my_container.end();/**blank*/ ) { if (*it % 2 == 1) { it = my_container.erase(it); } else{ it++; } }
如果你閱讀過erase()源碼或瞭解它是如何工作的,效能問題就顯而易見:erase()刪除一個元素的操作是被刪除元素後面的所有元素依次
向前移動一個元素的位置,然後刪除最後一個元素,時間複雜度為O(n^2)。
remove(),remove_if()的時間複雜度為O(n),刪除元素的操作如下所示:
template<class ForwardIt, class UnaryPredicate> ForwardIt remove_if(ForwardIt first, ForwardIt last, UnaryPredicate p) { ForwardIt result = first; for (; first != last; ++first) { if (!p(*first)) { *result++ = *first; } } return result; }
從前向後遍曆容器所有元素,將待保留的元素向前移動,佔據待刪除的元素的位置,remove_if()返回新的元素範圍(begin,end)中的end,
記為new_end_of_range ,再調用erase()刪除從new_end_of_range到my_container.end()之間的所有元素。
實際上,remove_if() 沒有刪除容器中的任何元素,它沒有改變my_container.end(), 調用remove_if()後容器元素個數不會改變!!刪除元素的工作
交給了erase().
Scott Meyers在他的”Effective STL”中關於此問題的討論中也使用了remove_if(),由此看來,他的確是提出了一些讓STL effective的建議。
深入學習STL迭代器失效問題:
在google中搜尋 stl iterator invalidation rules 可以獲得很多有關STL迭代器失效的有關內容。
References:
1. STL remove_if()
http://en.cppreference.com/w/cpp/algorithm/remove
2.More C++ Idioms/Erase-Remove http://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Erase-Remove
3.Effective STL, Item 32 - Scott Meyers
4.Cpp Invalid Iterators [對各種迭代器失效的情況進行了講解分類]
http://www.angelikalanger.com/Conferences/Slides/CppInvalidIterators-DevConnections-2002.pdf
5.以下是stackoverflow上關於在迭代時刪除容器中元素的討論:
http://stackoverflow.com/questions/1604588/iterate-vector-remove-certain-items-as-i-go
http://stackoverflow.com/questions/3747691/stdvector-iterator-invalidation?rq=1
http://stackoverflow.com/questions/2874441/deleting-elements-from-stl-set-while-iterating?rq=1
http://stackoverflow.com/questions/1038708/erase-remove-contents-from-the-map-or-any-other-stl
-container-while-iterating/1038761#1038761
http://stackoverflow.com/questions/799314/difference-between-erase-and-remove?rq=1
轉載本文請註明作者和出處[Gary的影響力]http://garyelephant.me,請勿用於任何商業用途!
Author: Gary Gao 關注互連網、分布式、高並發、自動化、軟體團隊