Rule9:在删除选项中仔细选择
本部分主要是讲不同的容器对删除特定元素的方法。
比如要删除容器Container< int > c中所有值为2016的对象,完成这项任务的方法因不同的容器类型而不同:没有一种方法是通用的。
- 如果你有一个连续内存容器(vector,deque,string)最好的方法是erase-remove方法
c.erase(remove(c.begin(),c.end(),2016),c.end());
这个方法也适用于list,但是list的成员函数remove更高效。
c.remore(2016);
当c是标准关联容器,适用任何叫做remove的方法是错误的,这样的容器没有叫做remove的成员函数,我们需要使用的适当方法是erase。 c.erase(2016);这不仅正确,而且高效,只需要花费对数时间。
接着我们考虑下面的问题,不是从c中去除每个特定值的对象,而是消除下面判断式返回真的每个对象:
bool badValue(int x);//返回x是否是bad
对于序列容器(vector,string,list,deque),我们需要使用remove_if。
如下代码:
c.erase(remove_if(c.begin(),c.end(),badValue),c.end());
当c是vector,string,deque时的最好方法
c.remove_if(badValue);
当c是list时的最好方法
对于标准关联容器,没有提供remove_if成员函数,有两种解决方案:
第一是容易编码的方式,但是效率不高效,使用remove_copy_if把我们需要的值拷贝到一个新容器中,然后把原容器的内容和新的交换。
AssocContainer< int > c; // c现在是一种标准关联容器
//goodValues用于容纳不删除的值的临时容器
AssocContainer< int > goodValues;
remove_copy_if(c.begin(), c.end(), inserter(goodValues,goodValues.end()),badValue);
c.swap(goodValues);//将goodValues的内容与c交换
我们可以通过直接从原容器删除元素来避免上面的开销,我们首先看如下的代码:
AssocContainer<int> c;
for (AssocContainer<int>::iterator i = c.begin();i!= c.end();++i)
{
if (badValue(*i)) c.erase(i);
}
千万不要这么做!!!会导致程序有未定义的行为。当关联容器的一个元素被删除时,指向那个元素的所有迭代器就已经失效了。当c.erase(i)返回时,i已经失效了,这样的循环显然会导致问题产生。
为了避免这个问题,我们必须保证在调用erase之前就得到c中下一个元素的迭代器。使用如下的方式。
AssocContainer<int> c;
...
for (AssocContainer<int>::iterator i = c.begin(); i != c.end();/*nothing*/ )
{ // 自增
if (badValue(*i)) c.erase(i++);
else ++i;
}
这种调用erase的解决方法可以工作,因为表达式i++的值是i的旧值,但作为副作用,i增加了。因此,我们把i的旧值(没增加的)传给erase,但在erase开始执行前i已经自增了。那正好是我们想要的。正如我所说的,代码很简单,只不过不是大多数程序员在第一次尝试时想到的。
我们进一步修改该问题。不仅删除badValue返回真的每个元素,而且每当一个元素被删掉时,我们也想把一条消息写到日志文件中。
对于关联容器,很容易,我们只需要插入写入文件的语句就行了。
对于vector,string和deque,我们需要使用erase的返回值,erase返回指向紧邻被删元素之后的元素的有效迭代器。
for (SeqContainer<int>::iterator i = c.begin();
i != c.end();){
if (badValue(*i)){
logFile << "Erasing " << *i << '\n';
i = c.erase(i); // 通过把erase的返回值
} // 赋给i来保持i有效
else
++i;
}
对于list结构,你可以像vector/string/deque一样或像关联容器一样对待list;两种方法都可以为list工作。
去除一个容器中有特定值的所有对象:
如果容器是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时记得后置递增它。
关于为什么需要使用c.erase(remove(c.begin(),c.end(),2016),c.end())和c.erase(remove_if());这种形式来删除特定元素?可以参见Rule32:我们可以先学习这个条款。
- Rule32:如果你真的想删除东西的话,就在类似remove算法后接上erase。
首先需要了解下 remove这个算法的内容。remove是STL中最糊涂的算法,容易让人产生误解。
remove是属于< algorithm >算法库中的一个方法,像其他的算法一样,remove接收指定他操作元素区间的一对迭代器,他不接受一个容器,所以remove不知道它作用于哪个容器。此外,Remove也不可能发现容器,因为没有办法从一个迭代器中获取对应的容器。
怎样从容器中除去一个元素呢?唯一的方法是调用那个容器的一个成员函数,几乎都是erase的某个形式。因为唯一从容器中除去一个元素的方法是在那个容器上调用一个成员函数,而且因为remove无法知道它正在操作的容器,所以remove不可能从一个容器中除去元素,因此从一个容器中remove元素不会改变容器中元素的个数。
如下代码:
我们需要记住的就是,remove并不是真正的删除元素,因为它做不到。
那么remove到底做了什么?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又能从容器中除去元素的函数:
调用这个remove函数是一个STL中的矛盾。在关联容器中类似的函数叫erase,list的remove也可以叫做erase。但它没有,所以我们都必须习惯它。
remove不能“真的”从一个容器中删除东西,和erase联合使用就变成理所当然了。你要记住的唯一其他的东西是remove不是唯一这种情况的算法。另外有两种“类似remove”的算法:remove_if和unique。