摘自《Effective STL》第9条
- 对于连续内存的容器(vector、deque 或 string),那么最好的办法是使用 erase-remove 的习惯用法:
1 #include <iostream> 2 #include <memory> 3 #include <vector> 4 #include <algorithm> 5 6 using namespace std; 7 8 int main() 9 { 10 vector<int> vec = {1,2,3,4,5,6,7,8,9,1,2,3,4,5,6,7,8,9}; 11 12 for (auto i : vec) { 13 cout << i << " "; 14 } 15 cout << endl; 16 17 vec.erase(remove(vec.begin(), vec.end(), 2), vec.end()); 18 19 for (auto i : vec) { 20 cout << i << " "; 21 } 22 cout << endl; 23 }
- 对于 list,使用 list 的成员函数 remove 更加有效
1 #include <iostream> 2 #include <memory> 3 #include <list> 4 #include <algorithm> 5 6 using namespace std; 7 8 int main() 9 { 10 list<int> c = {1,2,3,4,5,6,7,8,9,1,2,3,4,5,6,7,8,9}; 11 12 for (auto i : c) { 13 cout << i << " "; 14 } 15 cout << endl; 16 17 c.remove(2); 18 19 for (auto i : c) { 20 cout << i << " "; 21 } 22 cout << endl; 23 }
- 对于关联容器(set、multiset、map、multimap),这些容器没有 remove 成员函数。使用 remove 算法可能会覆盖容器的值,同时可能会破坏容器。
1 #include <iostream> 2 #include <memory> 3 #include <map> 4 #include <algorithm> 5 6 using namespace std; 7 8 int main() 9 { 10 multimap<int, int> c = {{1,1},{2,2},{3,3},{4,4},{5,5},{6,6},{7,7},{8,8},{9,9},{1,1},{2,2},{3,3},{4,4},{5,5},{6,6},{7,7},{8,8},{9,9}}; 11 12 for (auto i : c) { 13 cout << i.second << " "; 14 } 15 cout << endl; 16 17 c.erase(2); 18 19 for (auto i : c) { 20 cout << i.second << " "; 21 } 22 cout << endl; 23 }
这样做不仅正确,而且高效,只需要对数时间开销。关联容器的 erase 成员函数还有另外一个优点,即它是基于等价(equivalence)而不是相等(equality)。
如果想使用判别式函数, 怎么做?
- 对于序列容器(vector、string、deque、list),把 remove 的调用换成调用 remove_if 即可
1 #include <iostream> 2 #include <memory> 3 #include <vector> 4 #include <algorithm> 5 6 using namespace std; 7 8 class Foo 9 { 10 public: 11 Foo(int x):_x(x) {} 12 ~Foo() {} 13 void print() { cout << _x; } 14 int getX() { return _x; } 15 private: 16 int _x; 17 }; 18 19 bool badValue(Foo& f) 20 { 21 if (f.getX() == 2) { 22 return true; 23 } 24 return false; 25 } 26 27 int main() 28 { 29 vector<Foo> c; 30 31 for (int i = 0; i < 10; ++i) { 32 Foo f(i); 33 c.push_back(f); 34 } 35 36 for (int i = 0; i < 10; ++i) { 37 Foo f(i); 38 c.push_back(f); 39 } 40 41 for (auto i : c) { 42 i.print(); 43 cout << " "; 44 } 45 cout << endl; 46 47 c.erase(remove_if(c.begin(), c.end(), badValue), c.end()); 48 49 for (auto i : c) { 50 i.print(); 51 cout << " "; 52 } 53 cout << endl; 54 }
1 #include <iostream> 2 #include <memory> 3 #include <list> 4 #include <algorithm> 5 6 using namespace std; 7 8 class Foo 9 { 10 public: 11 Foo(int x):_x(x) {} 12 ~Foo() {} 13 void print() { cout << _x; } 14 int getX() { return _x; } 15 private: 16 int _x; 17 }; 18 19 bool badValue(Foo& f) 20 { 21 if (f.getX() == 2) { 22 return true; 23 } 24 return false; 25 } 26 27 int main() 28 { 29 list<Foo> c; 30 31 for (int i = 0; i < 10; ++i) { 32 Foo f(i); 33 c.push_back(f); 34 } 35 36 for (int i = 0; i < 10; ++i) { 37 Foo f(i); 38 c.push_back(f); 39 } 40 41 for (auto i : c) { 42 i.print(); 43 cout << " "; 44 } 45 cout << endl; 46 47 c.remove_if(badValue); 48 49 for (auto i : c) { 50 i.print(); 51 cout << " "; 52 } 53 cout << endl; 54 }
对于关联容器,则没有这么简单了。由于关联容器没有提供类似 remove_if 的成员函数。必须写一个循环来遍历 c 中的元素,并在遍历过程中删除元素。
但是其中有一个陷阱:当容器中的一个元素被删除时,指向该元素的所有迭代器都将变得无效,一旦 c.erase(i) 返回,i 将成为无效值。
为了避免这个问题,我们要确保在调用 erase 之前,有一个迭代器指向 c 中的下一个元素。最简单的办法是,对 i 使用后缀递增
1 #include <iostream> 2 #include <memory> 3 #include <set> 4 #include <algorithm> 5 6 using namespace std; 7 8 bool badValue(int x) 9 { 10 if (x == 2) { 11 return true; 12 } 13 return false; 14 } 15 16 int main() 17 { 18 set<int> c; 19 20 for (int i = 0; i < 10; ++i) { 21 c.insert(i); 22 } 23 24 for (auto i : c) { 25 cout << i << " "; 26 } 27 cout << endl; 28 29 for (auto i = c.begin(); i != c.end(); /* 什么也不做 */) { 30 if (badValue(*i)) { 31 c.erase(i++); // 把当前的 i 传给 erase,并且在 erase 开始前递增了 i,使 i 能够正确的指向下一个元素 ( 为什么 ? ) 32 } else { 33 ++i; 34 } 35 } 36 37 for (auto i : c) { 38 cout << i << " "; 39 } 40 cout << endl; 41 }
(答案:因为关联容器不是连续内存存储的,所以 i++ 后指向下个元素的迭代器还是有效的。如果是连续内存的容器(vector、string、deque),这样是错误的,因为调用 erase 不仅会使指向被删除元素的迭代器失效,也会使被删除元素之后的所有迭代器都无效(因为元素移动)。所以应该使用: i = c.erase(i); 因为 erase 会返回被删除元素)。
结论:
- 要删除容器中有特定值的所有对象
如果容器是 vector、string 或 deque,则使用 erase-remove 习惯用法
如果容器是 list,则使用 list::remove 成员函数
如果容器是一个标准关联容器,则使用它的 erase 成员函数
- 要删除容器中满足特定判别式(条件)的所有对象
如果容器是 vector、string 或 deque,则使用 erase-remove_if 习惯用法
如果容器是 list,则使用 list::remove_if 成员函数
如果容器是一个标准关联容器,则使用 remove_copy_if 和 swap,或者写一个循环来遍历容器中的所有元素,记住当把迭代器传给 erase 时,要对它进行后缀递增
- 要在循环内做某些(除了删除对象之外的)操作(比如记录日志)
如果容器是一个标准序列容器,则写一个循环来遍历容器中的元素,记住每次调用 erase 时,要用它的返回值更新迭代器。
如果容器是一个标准关联容器,则写一个循环来遍历容器中的元素,记住当把迭代器传给 erase 时,要对迭代器做后缀递增。