Center of STL Study
——最優秀的STL學習網站
條款32:如果你真的想刪除東西的話就在類似remove的演算法後接上erase
我將從remove的複習開始這個條款,因為remove是STL中最糊塗的演算法。誤解remove很容易,驅散所有關於remove行為的疑慮——為什麼它這麼做,它是怎麼做的——是很重要的。
這是remove的聲明:
template<class ForwardIterator, class T>ForwardIterator remove(ForwardIterator first, ForwardIterator last,const T& value);
就像所有演算法,remove接收指定它操作的元素區間的一對迭代器。它不接收一個容器,所以remove不知道它作用於哪個容器。此外,remove也不可能發現容器,因為沒有辦法從一個迭代器擷取對應於它的容器。
想想怎麼從容器中除去一個元素。唯一的方法是調用那個容器的一個成員函數,幾乎都是erase的某個形式,(list有幾個除去元素的成員函數不叫erase,但它們仍然是成員函數。)因為唯一從容器中除去一個元素的方法是在那個容器上調用一個成員函數,而且因為remove無法知道它正在操作的容器,所以remove不可能從一個容器中除去元素。這解釋了另一個令人沮喪的觀點——從一個容器中remove元素不會改變容器中元素的個數:
vector<int> v;// 建立一個vector<int> 用1-10填充它v.reserve(10);// (調用reserve的解釋在條款14)for (int i = 1; i <= 10; ++i) {v.push_back(i);}cout << v.size();// 列印10v[3] = v[5] = v[9] = 99;// 設定3個元素為99remove(v.begin(), v.end(), 99);// 刪除所有等於99的元素cout << v.size();// 仍然是10!
要搞清這個例子的意思,記住下面這句話:
remove並不“真的”刪除東西,因為它做不到。
重複對你有好處:
remove並不“真的”刪除東西,因為它做不到。
remove不知道它要從哪個容器刪除東西,而沒有容器,它就沒有辦法調用成員函數,而如果“真的”要刪除東西,那就是必要的。
上面解釋了remove不做什麼,而且解釋了為什麼它不做。我們現在需要複習的是remove做了什麼。
非常簡要地說一下,remove移動指定區間中的元素直到所有“不刪除的”元素在區間的開頭(相對位置和原來它們的一樣)。它返回一個指向最後一個的下一個“不刪除的”元素的迭代器。傳回值是區間的“新邏輯終點”。
舉個例子,這是v在調用remove前看起來的樣子:
如果我們把remove的傳回值存放在一個叫做newEnd的新迭代器中:
vector<int>::iterator newEnd(remove(v.begin(), v.end(), 99));
這是調用後v看起來的樣子:
這裡我用問號來標明那些在概念上已經從v中被刪除,但繼續存在的元素的值。
如果“不刪除的”元素在v中的v.begin()和newEnd之間,“刪除的”元素就必須在newEnd和v.end()之間——這好像很合理。事實上不是這樣!“刪除的”值完全不必再存在於v中了。remove並沒有改變區間中元素的順序,所以不會把所有“刪除的”元素放在結尾,並安排所有“不刪除的”值在開頭。雖然標準沒有要求,但一般來說區間中在新邏輯終點以後的元素仍保持它們的原值。調用完remove後,在我知道的所有實現中,v看起來像這樣:
正如你所見,兩個曾經存在於v的“99”不再在那兒了,而一個“99”仍然存在。一般來說,調用完remove後,從區間中刪除的值可能是也可能不在區間中繼續存在。大多數人覺得這很奇怪,但為什嗎?你要求remove除去一些值,所以它做了。你並沒有要求它把刪除的值放在一個你以後可以擷取的特定位置,所以它沒有做。有問題嗎?(如果你不想失去任何值,你可能應該調用partition或stable_partition而不是remove,partition在條款31中描述。)
remove的行為聽起來很可惡,但它只不過是演算法操作的附帶結果。在內部,remove遍曆這個區間,把要“刪除的”值覆蓋為後面要保留的值。這個覆蓋通過對持有被覆蓋的值的元素賦值來完成。
你可以想象remove完成了一種壓縮,被刪除的值表演了在壓縮中被填充的洞的角色。對於我們的vector v,它按照下面的表演:
- remove檢測v[0],發現它的值不是要被刪除的,然後移動到v[1]。同樣的情況發生在v[1]和v[2]。
- 發現v[3]應該被刪除,所以它記錄下v[3]的值應該被覆蓋,然後它移動到v[4]。這類似記錄v[3]是一個需要填充的“洞”。
- 發現v[4]的值應該被保持,所以它把v[4]賦給v[3],記錄下v[4]應該被覆蓋,然後移動到v[5]。繼續類似的壓縮,它用v[4]“填充”v[3]而且記錄v[4]現在是一個洞。
- 發現v[5]應該被刪除,所以忽略並它移動到v[6]。仍然記得v[4]是一個等待填充的洞。
- 發現v[6]是一個應該保留的值,所以把v[6]賦給v[4]。記得v[5]現在是下一個要被填充的洞,然後移到v[7]。
- 在某種意義上類似上面的,檢查v[7]、v[8]和v[9]。把v[7]賦給v[5],v[8]賦給v[6],忽略v[9],因為v[9]的值是要被刪除的。
- 返回指定下一個要被覆蓋的元素的迭代器,在這個例子中這個元素是v[7]。
你可以預想在v中值的移動情況像這樣:
正如條款33所解釋的,事實上當remove在刪除時覆蓋的值是指標時,會有重要的影響。但是對於本條款,知道remove不從容器中除去任何元素因為它做不到就夠了。只有容器成員函數可以除去容器元素,而那是本條款的整個要點:如果你真的要刪除東西的話,你應該在remove後面接上erase。
你要erase的元素很容易識別。它們是從區間的“新邏輯終點”開始持續到區間真的終點的原來區間的元素。要除去那些元素,你要做的所有事情就是用那兩個迭代器調用erase的區間形式(參見條款5)。因為remove本身很方便地返回了區間新邏輯終點的迭代器,這個調用很直截了當:
vector<int> v;// 正如從前v.erase(remove(v.begin(), v.end(), 99), v.end());// 真的刪除所有// 等於99的元素cout << v.size();// 現在返回7
把remove的傳回值作為erase區間形式第一個實參傳遞很常見,這是個慣用法。事實上,remove和erase是親密聯盟,這兩個整合到list成員函數remove中。這是STL中唯一名叫remove又能從容器中除去元素的函數:
list<int> li;// 建立一個list// 放一些值進去li.remove(99);// 除去所有等於99的元素:// 真的刪除元素,// 所以它的大小可能改變了
坦白地說,調用這個remove函數是一個STL中的矛盾。在關聯容器中類似的函數叫erase,list的remove也可以叫做erase。但它沒有,所以我們都必須習慣它。我們所處於的世界不是所有可能中最好的世界,但卻是我們所處的。(附加一點,條款44指出,對於list,調用remove成員函數比應用erase-remove慣用法更高效。)
一旦你知道了remove不能“真的”從一個容器中刪除東西,和erase聯合使用就變成理所當然了。你要記住的唯一其他的東西是remove不是唯一這種情況的演算法。另外有兩種“類似remove”的演算法:remove_if和unique。
remove和remove_if之間的相似性很直截了當。所以我不會細講,但unique行為也像remove。它用來從一個區間刪除東西(鄰近的重複值)而不用訪問持有區間元素的容器。結果,如果你真的要從容器中刪除元素,你也必須成對調用unique和erase,unique在list中也類似於remove。正像list::remove真的刪除東西(而且比erase-remove慣用法高效得多)。list::unique也真的刪除鄰近的重複值(也比erase-unique高效)。
(譯註:《C++標準程式庫》111頁5.6節有remove的詳細解釋)
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
class test
...{
public:
test(int m)...{i = m;}
test()...{i = 0;}/**//*
bool operator()(test j)
{
return i == j.i ;
}*/
bool operator==(test j)
...{
return i == j.i;
}
//private:
int i;
};
int main()
...{
vector<test> v; // 建立一個vector<int> 用1-10填充它
v.reserve(10); // (調用reserve的解釋在條款14)
for (int i = 1; i <= 10; ++i)
...{
test t;
t.i = i;
v.push_back(t);
}
cout << v.size(); // 列印10
test tt;
tt.i = 99;
v[3] = v[5] = v[9] = tt; // 設定3個元素為99
// remove(v.begin(), v.end(), 99); // 刪除所有等於99的元素
// cout << v.size(); // 仍然是10!
v.erase(remove(v.begin(), v.end(), 99), v.end());
for (vector<test>::iterator iter = v.begin(); iter != v.end(); iter++)
...{
cout << iter->i << " " ;
}
cout << endl;
return 0;
}
//ps:要重載operator==