1.為什麼要有仿函數
我們先從一個非常簡單的問題入手。假設我們現在有一個數組,數組中存有任意數量的數字,我們希望能夠計數出這個數組中大于10的數字的數量,你的代碼很可能是這樣的:
#include <iostream>
using namespace std;
int RecallFunc(int *start, int *end, bool (*pf)(int))
{
int count=0;
for(int *i=start;i!=end+1;i++)
{
count = pf(*i) ? count+1 : count;
}
return count;
}
bool IsGreaterThanTen(int num)
{
return num>10 ? true : false;
}
int main()
{
int a[5] = {10,100,11,5,19};
int result = RecallFunc(a,a+4,IsGreaterThanTen);
cout<<result<<endl;
return 0;
}
複制
RecallFunc()函數的第三個參數是一個函數指針,用于外部調用,而IsGreaterThanTen()函數通常也是外部已經定義好的,它隻接受一個參數的函數。如果此時希望将判定的門檻值也作為一個變量傳入,變為如下函數就不可行了:
bool IsGreaterThanThreshold(int num, int threshold)
{
return num>threshold ? true : false;
}
複制
雖然這個函數看起來比前面一個版本更具有一般性,但是它不能滿足已經定義好的函數指針參數的要求,因為函數指針參數的類型是
bool (*)(int)
,與函數
bool IsGreaterThanThreshold(int num, int threshold)
的類型不相符。如果一定要完成這個任務,按照以往的經驗,我們可以考慮如下可能途徑:
(1)門檻值作為函數的局部變量。局部變量不能在函數調用中傳遞,故不可行;
(2)函數傳參。這種方法我們已經讨論過了,多個參數不适用于已定義好的RecallFunc函數。
(3)全局變量。我們可以将門檻值設定成一個全局變量。這種方法雖然可行,但是不優雅,且非常容易引入Bug,比如全局變量容易同名,造成命名空間污染。
那麼有什麼好的處理方法呢?仿函數應運而生。
2.仿函數的定義
仿函數(Functor)又稱為函數對象(Function Object)是一個能行使函數功能的類。仿函數的文法幾乎和我們普通的函數調用一樣,不過作為仿函數的類,都必須重載operator()運算符。因為調用仿函數,實際上就是通過類對象調用重載後的operator()運算符。
如果程式設計者要将某種“操作”當做算法的參數,一般有兩種方法:
(1)一個辦法就是先将該“操作”設計為一個函數,再将函數指針當做算法的一個參數。上面的執行個體就是該做法;
(2)将該“操作”設計為一個仿函數(就語言層面而言是個class),再以該仿函數産生一個對象,并以此對象作為算法的一個參數。
很明顯第二種方法會更優秀,原因也在上一小節有所闡述。正如上面的例子,在我們寫代碼時有時會發現有些功能代碼,會不斷地被使用。為了複用這些代碼,實作為一個公共的函數是一個解決方法。不過函數用到的一些變量,可能是公共的全局變量。引入全局變量,容易出現同名沖突,不友善維護。
這時就可以用仿函數了,寫一個簡單類,除了維護類的基本成員函數外,隻需要重載operator()運算符 。這樣既可以免去對一些公共變量的維護,也可以使重複使用的代碼獨立出來,以便下次複用。而且相對于函數更優秀的性質,仿函數,還可以進行依賴、組合與繼承等,這樣有利于資源的管理。如果再配合模闆技術和Policy程式設計思想,那就更是威力無窮了,大家可以慢慢體會。Policy表述了泛型函數和泛型類的一些可配置行為(通常都具有被經常使用的預設值)。
STL中也大量涉及到仿函數,有時仿函數的使用是為了函數擁有類的性質,以達到安全傳遞函數指針、依據函數生成對象、甚至是讓函數之間有繼承關系、對函數進行運算和操作的效果。比如STL中的容器set就使用了仿函數less ,而less繼承的binary_function,就可以看作是對于一類函數的總體聲明了,這是函數做不到的。
//less的定義
template<typename _Tp> struct less : public binary_function<_Tp, _Tp, bool>
{
bool operator()(const _Tp& __x, const _Tp& __y) const
{ return __x < __y; }
};
//set的申明
template<typename _Key,
typename _Compare = std::less<_Key>,
typename _Alloc = std::allocator<_Key>>
class set;
複制
仿函數中的變量可以是static的,同時仿函數還給出了static的替代方案,仿函數内的靜态變量可以改成類的私有成員,這樣可以明确地在析構函數中清除所用的内容,如果用到了指針,那麼這個是不錯的選擇。有人說這樣的類已經不是仿函數了,但其實,封裝後從外界觀察,可以明顯地發現,它依然有函數的性質。
3.仿函數執行個體
我們先來看一個仿函數的例子:
#include <iostream>
#include <string>
using namespace std;
class Functor
{
public:
void operator() (const string& str) const
{
cout << str << endl;
}
};
int main()
{
Functor myFunctor;
myFunctor("Hello world!");
return 0;
}
複制
程式輸出:Hello world!。
可以見到,仿函數提供了第四種解決方案:成員變量。成員函數可以很自然的通路成員變量,進而解決上文最開始的那個問題。
class StringAppend
{
public:
explicit StringAppend(const string& str) : ss(str){}
void operator() (const string& str) const
{
cout<<str<<' '<<ss<<endl;
}
private:
const string ss;
};
int main()
{
StringAppend myFunctor2("and world!");
myFunctor2("Hello");
return 0;
}
複制
程式輸出:Hello and world!。
這個例子應該可以讓您體會到仿函數的一些作用:它既能像普通函數一樣傳入給定數量的參數,還能存儲或者處理更多我們需要的有用資訊。于是本小節開頭的問題就迎刃而解了:
#include <iostream>
using namespace std;
class IsGreaterThanThresholdFunctor
{
public:
explicit IsLessThanTenFunctor(int tmp_threshold) : threshold(tmp_threshold{}
bool operator() (int num) const
{
return num>10 ? true : false;
}
private:
const int threshold;
};
int RecallFunc(int *start, int *end, IsGreaterThanThresholdFunctor myFunctor)
{
int count=0;
for(int *i=start;i!=end+1;i++)
{
count = myFunctor(*i) ? count+1 : count;
}
return count;
}
int main()
{
int a[5] = {10,100,11,5,19};
int result = RecallFunc(a,a+4,IsLessThanTenFunctor(10));
cout<<result<<endl;
return 0;
}
複制
參考文獻
[1]仿函數.百度百科