C++ STL:Predicate vs. Function Object
所謂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++標準庫應該保證絕不出現本例所出現的情況,仍在討論之中,如果考慮程式的移植性,你應該永遠不依賴於程式細節。