STL中僞函數、函數對象(functor)初步理解(下)

好了,有了以上理論知識,就可以探討一下函數對象的用法了,接下來我會以STL中使用最頻繁的for_each做例子,來說明函數對象的原理和作用。

先查一下for_each的用法,在www.cplusplus.com上查到了比較詳細的解釋,原文如下:


Apply function to range

Applies function f to each of the elements in the range [first,last).

The behavior of this template function is equivalent to:


template<class InputIterator, class Function> Function for_each(InputIterator first, InputIterator last, Function f) { for ( ; first!=last; ++first ) f(*first); return f; }

Parameters

first, last
Input iterators to the initial and final positions in a sequence. The range used is [first,last), which contains all the elements between first and last, including the element pointed by first but not the element pointed by last.
f
Unary function taking an element in the range as argument. This can either be a pointer to a function or an object whose class overloads operator().
Its return value, if any, is ignored.

Return value

The same as f.


從上面這段文字首先可以看出,for_each是個區間操作函數,輸入的前兩個參數是起始迭代器和終點迭代器,而第三個參數則是一個function類型的參數,從下面的解釋不難看出,這個參數既可以是函數,又可以是函數對象(也就是重載了()的類),也就是說,雖然函數對象在STL中很多地方已經取代了函數指針,但並不是函數指針就被完全拋棄了,因此,for_each所操作的對象有兩種:函數對象和全局函數指針,以此類推,STL中所有其他range function也都具有相似的特徵呢?很有可能,而事實上,幾乎所有區間操作函數確實也全都是這樣的。


好,有了以上的解釋,我們就可以試試for_each的用法了。先從最簡單的情況開始,試一試一元全局函數+函數指針的情況,看看以下代碼片段。

#include <iostream>      
#include <vector>      
#include <iostream>      
#include <algorithm>      
    
using namespace std;      
    
void printTest(const int &data)      
{      
    cout << data << endl;      
}      
    
int main()      
{      
    int test[] = {1, 2 , 3 , 4 , 5};      
    vector<int> v(test, test + sizeof(test) / sizeof(int));      
    
    for_each(v.begin(), v.end(), printTest);      
    
    return 0;      


這個代碼片段中,給for_each傳的參數是全局函數指針,可以通過編譯,這也說明了STL可以接受正確的函數指針作爲參數,那麼,可不可以把這個函數指針轉化成functor?當然可以,而使用的方法正是之前提到的,用ptr_fun來轉,也就是這樣:

for_each(v.begin(), v.end(), ptr_fun(printTest));

view 

仍然可以通過編譯,也就是說,正確的全局函數指針轉化成函數符是沒有問題的。OK,下面試一下binary glocal function的情況,把原代碼做一下微調,變成下面的樣子:

#include <iostream>  
#include <vector>  
#include <iostream>  
#include <algorithm>  
  
using namespace std;  
  
void printTest(const int &data, const int &num)  
{  
    int result = data+num;  
    cout << "data:"<<data<<"num:"<<num<<",result:"<<result << endl;  
}  
  
int main()  
{  
    int test[] = {1, 2 , 3 , 4 , 5};  
    vector<int> v(test, test + sizeof(test) / sizeof(int));  
  
    for_each(v.begin(), v.end(), printTest);  
  
    return 0;  
}  

變化的部分是printTest函數,由unary function變成了binary function,主函數不變,編譯出錯,出錯的原因很簡單,對於帶兩個形參的全局函數,如果直接把函數指針傳給for_each,程序不會知道哪個形參應該接收變量,因此會出錯,正確的做法應該是先綁定參數,然後再傳給for_each即可,這裏,就用到了STL裏的參數綁定器,也就是bind1st和bind2nd,拿bind1st做例子,這個函數的聲明是這樣的:

template <class Operation, class T> binder1st<Operation> bind1st (const Operation& op, const T& x);

再看一下對於參數的描述:

op

      Binary function object derived from binary_function.

x

      Fixed value for the first parameter of op.

第一個參數op是我們需要的operation,而第二個參數則是需要綁定的參數的值,也就是說,按照這種方式,如果我想在上面的代碼中調用bind1st應該是類似這種形式:bind1st(printTest , 3),但是很明顯,直接這麼用肯定是錯的,因爲printTest並不是一個繼承自binary_function的函數,這裏就用到了剛纔所用到的ptr_fun了,這個函數可以把global function轉化成一個函數對象,而對於有兩個參數的函數對象來說,則會返回一個繼承自binary_function的函數對象,因此,對於這個程序,正確的綁定應該是這樣:bind1st(ptr_fun(printTest) , 3),在這個表達式裏,printTest的第一個參數被綁定成了3,好,再次編譯,因該沒問題了吧?錯了,仍然有問題,問題如下:

error C2535: 'void std::binder1st<_Fn2>::operator ()(const int &) const' : member function already defined or declared.

函數存在重複定義,而重複部分函數在STL中的xFunctional頭文件中,重複的函數爲:

std::pointer_to_binary_function<const int &,const int &,void,void (__cdecl *)(const int &,const int &)>

這裏我們知道,ptr_fun可以把全局函數轉換成一個pointer_to_binary_function,但是該函數的默認參數正式和printTest具有一樣形式的函數,也就是說,兩個函數的指針形式是一樣的,因此定義重複,我們把原代碼再次改一下,把printTest的聲明改成這種形式:void printTest(const int &data, int num),再次編譯、運行,產生的結果如下:

從截圖可以清晰的看到,參數3被綁定到了printTest的第一個參數上,而第二個參數則會接收容器中的變量,該段代碼產生的效果和手寫循環一樣。bind2nd的用法和bind1st一樣,只不過它是把變量綁定到函數第二個參數上,假如用bind2nd(ptr_fun(printTest) , 3)代替上面的代碼的話,會輸出這樣的結果:


從截圖上可以看到,3被綁定到了第二個參數上。


討論完了全局函數的情況,該討論一下類中成員函數的情況了,其實成員函數和全局函數在轉化成functor的原理上都是一樣的,都是需要借用一個適配器,這個適配器可以讓functor所需要的typrdef生效,而成員函數所需要的適配器則是mem_fun和mem_fun_ref,關於這兩個函數的區別在上面已經提到,不做贅述,僅舉一例來說明這個函數的用法。


在上個程序中,printTest是一個全局函數,可以被直接調用,現在,我們把它封裝到一個類中,經過改造後的代碼如下:

#include <iostream>    
#include <vector>    
#include <iostream>    
#include <algorithm>    
  
using namespace std;    
  
class print  
{  
private:  
    int data;  
  
public:  
    print(){}  
    print(int _data)  
    {  
        data = _data;  
    }  
    void printTest(int num)    
    {    
        int result = data+num;  
        cout <<"data: "<<data<<" num: "<<num<<" result: "<<result << endl;    
    }    
};  
int main()    
{    
    print p1(1);  
    print p2(2);  
    print p3(3);  
  
    vector<print> v;  
    v.reserve(3);  
    v.push_back(p1);  
    v.push_back(p2);  
    v.push_back(p3);  
  
    for_each(v.begin(),v.end(),bind2nd(mem_fun_ref<void,print,int>(&print::printTest),3));    
  
    return 0;    
}    

 pla

那個for_each語句中,mem_fun_ref後面一連串的模板參數讓人看起來有點暈,但是一點都不復雜,第一個參數是函數對象的返回類型,在這個程序裏,printTest返回的是個void,第二個是調用函數的類,即是print類,第三個是綁定的參數類型,在printTest中就是int。之所以顯式聲明出模板參數,是爲了演示需要,如果你願意,你完全可以去掉哪些參數,而直接使用mem_fun_ref(&print::printTest),這是完全沒有問題的,模板函數不必一直都顯式指定參數類型。還有一點需要注意的就是,對於成員函數的變量綁定,肯定是用bind2nd綁定器的,因爲默認狀態下,第一個變量已經綁定給了調用函數的對象上,因此只有一個變量可以綁定,而mem_fun_ref也明確規定了轉化的目標函數只能無參數或者具有一個參數。


函數對象和綁定器是密切相關的,本文對綁定器原理方面並沒有做過多的敘述,如果各位有興趣,可以自行搜索一下binder,以加深對函數對象的理解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章