什麼是仿函式?

thinking~發表於2020-05-27

轉自:https://blog.csdn.net/K346K346/article/details/82818801

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.仿函式例項
我們先來看一個仿函式的例子。

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");
}

編譯執行輸出:

Hello and world!

這個例子應該可以讓您體會到仿函式的一些作用:它既能像普通函式一樣傳入給定數量的引數,還能儲存或者處理更多我們需要的有用資訊。於是仿函式提供了第四種解決方案:成員變數。成員函式可以很自然地訪問成員變數,從而可以解決第一節“1.為什麼要有仿函式”中提到的問題:計數出陣列中大於指定閾值的數字數量。

#include <iostream>
using namespace std;

class IsGreaterThanThresholdFunctor
{
public:
    explicit IsGreaterThanThresholdFunctor(int t):threshold(t){}
    bool operator() (int num) const
    {
        return num > threshold ? 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, IsGreaterThanThresholdFunctor(10));
    cout << result << endl;
}

編譯執行輸出:

 3

相關文章