所谓Predicate(判断式),就是返回Boolean值的函数或者函数对象。对STL而言,并非所有返回Boolean的函数都是合法的Predicate。这可能会导致出人意料的结果——
#include<iostream>
#include<list>
#include<algorithm>
using namespace std;
class Nth {
private:
int n;
int count;
public:
Nth(int n) :n(n), count(0) {
}
bool operator()(int) {
return ++count == n;
}
};
int main(){
list<int> coll{ 1,2,3,4,5,6,7,8,9 };
cout << "coll:";
for (auto elem : coll) {
cout << elem << " ";
}
cout << endl;
auto pos = remove_if(coll.begin(), coll.end(), Nth(3));
coll.erase(pos,coll.end());
cout << "Removed:";
for (auto elem : coll) {
cout << elem << " ";
}
cout << endl;
return 0;
}
结果和想象中的不太一样,按照我们所写的类,调用到第n次的时候,应该就能够得到对应的删除位置。但结果却删除了两次,第3个元素和第6个元素都被删除了,这与remove_if
函数的内部构造有关——
template<typename ForwIter,typename Predicate>
ForwIter std::remove_if(ForwIter beg,ForwIter end,Predicate op)// [【1】第一处op
{
beg = find_if(beg,end,op);// 【2】第二处op
if(beg==end){
return beg;
}
else{
ForwIter next = beg;
return remove_copy_if(++next,end,beg,op);// 【3】第三处op
}
}
从STL对remove_if
函数的内部(本例基于GCC 4.9.2,其他环境不做讨论)实现不难发现问题所在,从传入的第一份op起,中间要调用一次find_if
函数来找到迭代器所指向元素的位置,但第二处所调用的op是按值传递,即不改变op的原始状态,因此,在第三处op调用的时候,依旧是从零开始,那么首先在remove_copy_if
内部删除了beg所指向的第一个元素,然后在后面的迭代中继续删除了第二个“第三个元素”,即第六个元素。
这种行为不能说是一种错误,C++标准并未规定Predicate是否可被算法复制。因此,为了获得C++标准保证的行为,你需要保证你所传递的function object不会因复制或调用次数而异,做到这一点,可以将operator函数声明为const成员函数,但是这样做又有些作茧自缚,毕竟很多时候还是需要改变数据成员的。
但实际上可以不用这么麻烦,我们发现造成这一现象的罪魁祸首是迪调用了一次find_if
函数,导致多计算一次pos的位置,那么不妨考虑使用while循环替代find_if的做法,Visual Studio 2017采用了这种方案——
template<class _FwdIt,
class _Pr>
_NODISCARD inline _FwdIt remove_if(_FwdIt _First, const _FwdIt _Last, _Pr _Pred)
{ // remove each satisfying _Pred
_Adl_verify_range(_First, _Last);
auto _UFirst = _Get_unwrapped(_First);
const auto _ULast = _Get_unwrapped(_Last);
_UFirst = _STD find_if(_UFirst, _ULast, _Pass_fn(_Pred));
auto _UNext = _UFirst;
if (_UFirst != _ULast)
{
while (++_UFirst != _ULast)
{
if (!_Pred(*_UFirst))
{
*_UNext = _STD move(*_UFirst);
++_UNext;
}
}
}
_Seek_wrapped(_First, _UNext);
return (_First);
}
在VS2017上运行了一下,结果变得正常了。但,C++标准库应该保证绝不出现本例所出现的情况,仍在讨论之中,如果考虑程序的移植性,你应该永远不依赖于程序细节。