在 C++ 裡傳遞、儲存函式 Part 2:Function Object

| | 4 Comments| 09:08
Categories:

前一篇大致介紹了 C 語言裡的 function pointer 了,而這一篇,則是來大概介紹 C 的 function object 了!

簡介

Function Object 又稱作「functor」,算是一個物件導向概念下的東西,他基本上是把函式視為一個物件來做操作。

而如果想要自己寫一個的 function object 的話,可以透過自訂一個新的類別,去重新定義他的 call operator、也就是 operator() 來做到;此一來,這個類別的物件就可以用類似一般函式的方法來當作函式使用了。

比如說下面的這個 class Output,就可以算是一個 function object 的類別了∼

class Output
{

public:
void operator() ( int x )
{
std::cout << x <<
", ";
}
};

在有了 Output 後,我們就可以把它的物件用看起來很像是函式的方法來使用;例如下面就是簡單的使用例子:

Output pout;
pout( 10 );

而同時,function object 的概念可以和 template 整合地非常好。基本上,在 C 的 STL 裡也大量地使用了這樣的概念來實作各種演算法;像是 STL 裡面的 for_each()sort() 等等的演算法(必須 include「algorithm」這個 STL 的 header 檔),都是透過 funciton object 加上 template 來做的∼像以 for_each() 這個演算法來說,他實際上的寫法就是下面這樣的一個 template function(程式碼內容取自 Microsoft Visual C 2010 所提供的 algorithm 檔):

template<class _InIt, class _Fn1>
inline _Fn1 _For_each(_InIt _First, _InIt _Last, _Fn1 _Func)
{
// perform function for each element
for (; _First != _Last; _First)
_Func(*_First);
return (_Func);
}

可以看的出來,for_each() 基本上就是一個 template 函式,裡面是透過 for 迴圈來對 iterator 來做操作、掃過所指定的整個資料範圍。而 function object 也就是單純地透過 template 的形式,當作其函式的中一個參數(_Func)傳到裡面的;而要呼叫傳入的 function object 的時候,也就是直接呼叫他的 operator() 就可以了∼

而透過類似這樣的寫法,其實也是可以大量地簡化程式開發的時間、以及所需編寫的程式碼的數量的!

使用說明

在定義好自己的 function object 後,實際要套用到 STL 內建的演算法的時候,則是可以以下面的形式,來把一個新的、型別是 Output 的物件傳給 for_each() 使用:

vector<int> v;
...
for_each( v.begin(), v.end(), Output() );

如此一來,在執行 for_each() 的時候,就會把 v 這個 vector 裡的每一項都當作參數、依序傳給 Output 的 來執行自己定義的 Output::perator() 了∼而這樣的寫法,其實就相當於直接把程式寫成下面的形式:

vector<int> v;
...
for( vector<int>::iterator it = v.begin(); it != v.end(); it )
std::cout << *it <<
", ";

而實際在定義的 function object 的時候,其實不一定要使用 class 來做,也可以用 structure、甚至直接用一般的函式也是可以的。像是比如說寫成下面這樣:

void OutputFunc( int x )
{
std::cout << x <<
", ";
}

for_each( v.begin(), v.end(), OutputFunc );

這樣雖然 OutputFunc 本身只是一個 function,不過也是可以拿來當作 object 用的∼

另外,如果搭配 C 0x 的 Lambda expression(參考)的話,是可以更方便地使用的!像下面這行程式,和上面這段程式,是會有同樣的效果的。

for_each( v.begin(), v.end(), [](int x){ std::cout << x << ", "; } );

Function Object 的優點

基本上,function object 和 function pointer 可以做的事其實差不多,不過和 function pointer 相比,function object 是有一些優點的∼一般來說,function object 的優點主要是兩點:一個是他可以透過 inline 來提升效能,其次是他可以在 function 內部來記錄狀態

前者除了程式碼本身的寫法外,主要更取決於編譯器的最佳化能力,之後 Heresy 應該會用 MSVC10 和 gcc 測試一下 function pointer 和 function object 的效能,而在這邊就暫時跳過了。

而在狀態紀錄的功能方面,由於 function object 本身就可以是一個自行定義的類別物件,所以其實內部要有多少變數用來記錄狀態,其實是相當自由的!雖然 function pointer 也不是完全沒有辦法同樣的事,但是相對之下,使用 function object 應該還是會比較簡單的∼

這邊舉個例子,假如我們定義了一個有三項的 vector 結構 Vector3

struct Vector3
{
Vector3(
const int& a, const int& b, const int& c )
{
m_iaData[0] = a;
m_iaData[1] = b;
m_iaData[2] = c;
}

int m_iaData[3];
};

而如果又建立了一個 vector< Vector3 > 的資料,那想要用 STL 的 sort() 來針對某一項排序的話,可以建立一個比較大小用的 function object:

class Comparsion
{

public:
int idx;
Comparsion(
const int& i ) : idx( i ){};

bool operator() ( const Vector3& v1, const Vector3& v2 ) const
{
return v1.m_iaData[idx] < v2.m_iaData[idx];
}
};

這個 class 裡面除了定義了比較大小用的 operator() 外,也另外宣告出了一個成員變數 idx、以及對應的建構子,用來指定要比較的項目;這樣要使用的時候,只要透過 constructor 來指定要比較的項目就可以了∼像下面的寫法,就可以讓 STL 的 sort() 使用我們自己定義的 Comparsion 來針對 Vector3 的第一項做比較、排序了∼

vector< Vector3 > v3;
...
sort( v3.begin(), v3.end(), Comparsion(1) );

此外,其實使用 function object 也更能符合物件導向的概念,像是如果透過 operator overload 的話,也可以讓一個 function object 裡有多個不同的 operator(),進而讓單一的 function class 有更多的功能。

STL 提供的 Function Object

除了可以自己定義 function object 外,C STL 的 <functional> 這個 header 檔裡,也有提供一些已經定義好的 function object 可以直接拿來用;包含了三類,共十五個 template function object:

  • 算術(Arithmetic)
    • plusminusmultipliesdividesmodulusnegate
  • 比較(Comparison)
    • equal_tonot_equal_togreaterlessgreater_equalless_equal
  • 邏輯計算(Logical)
    • logical_andlogical_orlogical_not

而每個 function object 的內容實際上都非常地簡單,在這邊就不詳列了;有興趣的人也可以參考 cplusplus.com 文章裡「operator classes」的說明。

STL Binder

除了這些預先定義好的 function object 外,STL 在 <functional> 裡,也還有提供一些其他功能,可以幫助程式開發者使用這些 function object;像是「binder」、「negators」、「conversors」都算是這類的東西。

要怎麼用呢?首先,STL 有定義出:只有一個參數的「一元函式」(unary_function)和有兩參數的「二元函式」(binary_function)。而 STL 裡不同的演算法,視需求的不同,要傳入的 function object 可能會是一元的(例如 for_each()),也有可能是二元的(例如 sort())。

像上面所列的 STL function object 裡,除了 negatelogical_not 是一元函式外,其他的都是二元函式。而一元函式因為本身的特性,本來就不太可能變成二元函數來使用;但是有的時候,我們可能會希望透過指定其中一項參數的值,來把二元函式變成一個一元函式來使用,這時候就可以透過 STL 的 binder 來做了∼

STL 的 binder 有 bind1st()bind2nd() 兩個函式,前者是用來指定二元函式的第一個參數的值,後者則是用來指定第二個參數的值。這邊 Heresy 用 count_if() 來做簡單的使用範例:

struct GreaterThan
{
int x;
GreaterThan(
int value ) : x( value ){};

bool operator() ( int value )
{
return value > x;
}
};
vector<int> v;
...
int num = count_if( v.begin(), v.end(), GreaterThan( 4 ) );

像是上面的例子,我們可以透過 count_if() 以及自己定義的 GreaterThan 來計算 vector v 裡面,值大於某個數的項目個數;不過實際上,我們也可以透過 STL 的 binder 和 greater 來做到同樣的事,而不用自己去定義這個 function object。寫法如下:

num = count_if( v.begin(), v.end(), bind2nd( greater<int>(), 4 ) );  

其中,「bind2nd( greater<int>(), 4 )」就是透過將 STL 內建的 greater 這個內建的二元函式的第二個參數指定成「4」,讓他變成一個「判斷值是否大於 4」的一元函式;如此,就可以給 count_if() 這類需要一元函式的演算法來使用了∼

不過,如果要把 binder 可以套用在自己定義的 function object 上的話,那 function object 的 clasee 就必需要繼承 binary_function 這個 class,如此才能使用 binder 來做轉換。如果需要更強大、自由的 bind 功能的話,可能就要使用 Boost C Libraries 裡的 bind 函式庫了∼而這部份就以後有時間再說了。

使用物件本身的函式來當 Function Object

除了 binder 外,STL 也有提供所謂的「conversors」,可以把 function pointer 或是物件本身的成員函式轉換成 function object、來在 STL 的演算法裡使用。而 conversors 這部份的函式有三種,分別是:ptr_fun()mem_fun() 以及 mem_fun_ref()

其中,ptr_fun() 是可以將 function pointer 轉換成為 function object;而他本身是兩個 function,分別對應到一元函式和二元函式。不過 Heresy 不打算講這個,有興趣的請自行參考 cpluplus.com 的文章

mem_fun()mem_fun_ref() 則是可以將物件的成員函式轉換成 function object 來使用;如果以套用到 STL 的 for_each() 來說的話,就是會去呼叫指定的資料範圍裡每一項資料的特定函示了!下面是一個簡單的例子:

vector< string > vString;
//...
for_each( vString.begin(), vString.end(), mem_fun_ref( &string::clear ) );

這個例子會透過 for_each() 去呼叫 vString 裡每一個字串的 string::clear() 函式,藉此把 vString 裡的每一個字串都清空。也就是相當於下面的程式:

for( vector<string>::iterator it = vString.begin(); it != vString.end();    it )
(*it).clear();

mem_fun()mem_fun_ref() 兩者的差別,則在於 mem_fun() 是透過 pointer 的形式來呼叫,而 mem_fun_ref() 則是透過 reference 的形式來呼叫;要用哪一個就要視自己的資料形式而定了∼如果資料本身是指標的話,就用 mem_fun(),而資料是物件的話,就使用 mem_fun_ref()。下面就是使用 pointer 版的例子:

vector< string* > vStrPtr;
//...
for_each( vStrPtr.begin(), vStrPtr.end(), mem_fun( &string::clear ) );

這一篇對於 function object 的基本介紹大概就先寫到這了∼下一篇,還會再針對 TR1 對於 function object 的一些擴充,來做進一步的說明的∼

4 thoughts on “在 C++ 裡傳遞、儲存函式 Part 2:Function Object”

  1. 不好意思,指點小弟一下

    原文中

    struct Vector3
    {
    Vector3( const int& a, const int& b, const int& c )
    {
    m_iaData[0] = a;
    m_iaData[1] = b;
    m_iaData[2] = c;
    }

    int m_iaData[3];
    };

    依照比較的Fuction Object 內的值是不是只有比較到”m_iaData[0]”?
    ———————-分隔————————-
    原文中
    vector< Vector3 > v3;

    sort( v3.begin(), v3.end(), Comparsion(1) );

    如果假設要比較三個的話話,是不是程式碼如下

    vector< Vector3 > v3;
    Vector3 AA=(10,0,0);
    v3.push_back(AA);
    Vector3 BB=(30,0,0);
    v3.push_back(BB);
    Vector3 CC=(20,0,0);
    v3.push_back(CC);

    sort( v3.begin(), v3.end(), Comparsion(1) );

  2. to 豆花

    1. Comparsion 這個 function object 可以針對任意一項來做比較,要針對哪一項做比較,可以透過建構時的參數自行調整。

    2. 不太懂你的意思,不過如果你是希望 Vector3 裡面每一項值都拿來比較的話,你必須要自己去定義該怎麼比較。

  3. 2.(有錯的地方請幫我指正)
    因為struct Vector中建構子有三個參數所以我填入三個數值給他
    ex:Vector3 AA(10,0,0);
    這樣Vector3中的陣列才會有值

    Comparsion() 裡面填的數值,ex:Comparsion(0),代表是比較陣列裡的第0 個element。這是我打的程式碼,可以麻煩幫我看一下嗎http://ideone.com/w6fBR0

  4. to 豆花

    你如果是要根據第 0 項的值來排序,這樣寫應該沒問題。
    個人建議把順序打亂再做測試比較有意義。

發佈回覆給「heresy」的留言 取消回覆

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *