慎重选择删除元素的方法

摘自《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 时,要对迭代器做后缀递增。

    

 

posted @ 2016-06-23 13:20  Kjing  阅读(475)  评论(0编辑  收藏  举报