在 C++ 裡傳遞、儲存函式 Part 3:Function Object in TR1

| | 4 Comments| 11:38
Categories:

在前一篇的《Part 2:Function Object》裡,基本上已經大致介紹了 C STL 裡的 function object 了。不過實際上,光靠 STL 的部分,其實感覺還是有點不足;所以後來在通稱 TR1 的《C Technical Report 1》(參考維基百科)裡,也有再提供不少用來強化 function object 使用的元件、函式。像是可以用來儲存 function object 的 template class function、更通用的 bind()mem_fn() 等等,都可以讓 function object 在使用上更方便的。

不過,要使用 TR1 是需要編譯器支援的。目前來說,Microsoft Visual C 2008 和 gcc 4 以後的版本,應該是都有支援的;如果編譯器不支援的話,可能就要考慮透過 Boost C Libraries 來使用 TR1 的功能了。Heresy 這邊是使用 Visual C 10.0(Visual C 2010)和 Gentoo 上的 gcc 4.4.3 來做編譯測試的;其中 VC10 可以不做任何調整直接編譯,gcc 則是額外加上「-std=c 0x」來編譯。

而這一篇,就是來介紹一下 TR1 裡,Heresy 個人覺得比較有用的 function object 使用方法了∼不過這部分 Heresy 也是自己邊看邊學邊寫的,所以有的東西可能也不是了解得很徹底,如果有錯誤的話,也麻煩見諒、並請幫忙指正了。

儲存 Function Object

由於不同的 function object 就是不同的型別,雖然 STL 裡大多是用 template 函式的形式來處理,所以沒什麼問題,但是如果想要在 C 的標準語法裡宣告一個通用的型別來記錄不同的 function object 的話,其實並不容易。

不過,在 C TR1 中,它提供了「Polymorphic Function Wrappers」,也就是「function」這個 template class,可以輕易地解決這個問題。function 在程式碼的編寫上,大致上就像下面這樣子:

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

};

void OutputFunc( int x )
{
std::cout << x <<
", ";
}
std::function< void(int) > func1 = Output();
std::function<
void(int) > func2 = OutputFunc;
std::vector< std::function<
void(int) > > foArray;

如同上面的例子,這樣透過 TR1 的 function 這個 template class,我們就可以輕鬆地把同類型的函式以及 function object,用變數、陣列等等各式各樣的形式記錄下來了∼這樣的功能在許多時候是相當方便的。

基本上,function 就是一個 template 的類別,而要使用的時候,必須要指定 function object 的形式,也就是他的 operator() 所接受的參數、以及回傳的型別。像上面的例子裡的「void(int)」就是代表他接收一個 int 的參數,並且不回傳任何值。

而如果有多個參數的時候,也只要像一般在宣告函式的時候,用逗號(,)把參數的型別區隔開就可以了;例如「function< bool(const int&,const int&) >」就可以表示一個 int 的 comparsion function object,他要傳入兩個 const 的 int 參考當作變數、並且回傳一個 bool,如此就可以拿來給 STL 的 sort() 用。下面是一個簡單的使用範例:

vector<int> v;
...
std::function< bool(const int&,const int&) > func1 = less<int>();
std::sort( v.begin(), v.end(), func1 );

而基本上,由於 function 本身是 template 的類別,所以可以對應各種類型的 function object,在使用上是非常自由的!程式開發者也不必為了要讓不同的類別可以轉換,而另外去制定上層的抽象類別來做繼承,可以有效地簡化使用 function object 時的程式開發時間。

TR1 bind() 函式版

在前一篇文章裡有提過,STL 本身有提供 bind1st()binsd2nd() 這兩個 binder,可以把二元的 function object 轉換成一元函式來使用。而 TR1 則是又追加了更通用的 bind() 系列函式(函式的數量多得很誇張…),可以更廣泛地來「轉換」function object。

STL 提供的 bind1st()binsd2nd() 基本上只適用於繼承自 binary_function 的 function object,而 TR1 的 bind(),則是適用於絕大部分的函式。假設我們現在有一個需要三個參數的函式 output_vec3() 如下:

void output_vec3( int x, int y, int z )
{
std::cout << x <<
"/" << y << "/" << z << std::endl;
}

他基本上需要傳入 xyz 三個參數,然後在函式內透過 ostream 把這些內容作輸出。那如果 z 的值都固定是 0 的時候,就可以透過 TR1 的 bind() 來把 output_vec3() 這個需要三個參數的函式,轉換為一個只需要 xy 兩個參數的函式 fOV_z0()∼寫法如下:

std::function< void(int, int) > fOV_z0
= std::bind( output_vec3, std::placeholders::_1, std::placeholders::_2, 0 );

看起來很長?不過這主要是 Heresy 想要強調這些東西所屬的 namespace 的關係,如果使用 using namespace 的話,則可以讓他看起來比較乾淨:

using namespace std;
using namespace std::placeholders;

function<
void(int, int) > fOV_z0 = bind( output_vec3, _1, _2, 0 );

這樣的程式結果,fOV_z0 會是一個要求輸入兩個 int 當作參數的 function object,而執行它的結果,就相當於去呼叫 output_vec3() 時、自動把第三個參數的值填 0;也就是下面這兩行程式的結果會是相同的:

fOV_z0( 1, 2 );
output_vec3( 1, 2, 0 );

而回過頭看 bind(),他的第一個參數是要轉換的函式,以這個例子來說就是 output_vec3();而接下來的參數數量則是視要轉換的函式的參數個數而定,由於 output_vec3() 需要傳入三個參數,所以這邊也依序要有三個對應參數。

這邊可以看到前兩個參數是使用了特別的物件「placeholder」,也就是例子裡的「_1」、「_2」;它們的用處是把本來的參數對應到新的參數的順序,也就是在執行 fOV_z0() 的時候,會把第一個參數,當作 _1,第二個參數當作 _2,傳到 output_vec3() 裡。而除了前兩個變數式 placeholder 外,第三個參數,就是指定的值(這個例子就是 0)了∼也因此,其實也可以把 fOV_z0() 看作是:

inline void fOV_z0( int v1, int v2 )
{
output_vec3( v1, v2, 0 );
}

而也因為 placeholder 是用來代表位置的對應的,所以我們也可以用來做參數位置的交換。例如下面的例子:

bool cLess( const int& a, const int& b )
{
return a < b;
}
function< bool(int, int) > cGreater = bind( cLess, _2, _1 );

這樣寫的話,就是相當如寫了一個轉換的介面,把 cLess() 的參數順序顛倒過來,而呼叫 cGreater( 10, 5 ) 就相當於是去呼叫 cLess( 5, 10 ) 了∼

不過在使用時可能要注意的是,placeholder 的數量是有限制的,而數量是由函式庫的實作決定的,所以到底有幾個,可能就要看使用的開發環境了。以微軟的 Visual C 10(VC2010)來說,他的 placeholder 只有定義 _1_10,也就是要這樣使用的話,變數的數量最多就是十個了∼不過實際上,函式的參數數量會超過十個的機率應該也不大就是了,所以這個限制應該是還好。

TR1 bind() function object class 版

前面一段講的 bind() 都是套用在一般的 function 上,而如果要應用在 function object class 上呢?也是可以的,但是由於一個類別可能會有多個不同的 operator(),所以感覺似乎某些地方會有些限制、也比較麻煩,Heresy 自己也有不少地方還沒全弄懂,不過這邊還是先大概介紹一下。

最直覺、基本的用法,大概就是像下面這段程式碼,他在 GCC 是可以編譯過的:

#include <stdlib.h>
#include <functional>
#include <iostream>

using namespace std;
using namespace std::placeholders;

class CTest
{

public:
int operator()( int a, int b )
{
return a - b;
}
};

int main( int argc, char** argv )
{
function< int(int) > func = bind<int>( CTest(), _1, 10 );
return 0;
}

這裡就是定義了一個名為 CTest 的 function object class,然後就如同使用一般函式時一樣,搭配 bind() 來使用。不過,這邊可以發現在 bind() 的部分,和之前用在函式上時多了一個 template 型別的指定,這是用來指定回傳值的型別的;像上面的程式碼就是「bind<int>( CTest(), _1, 10 )」,其中「<int>」就是指定這個 function object 回傳的型別是 int。

但是這樣的程式,在 VC10 是沒辦法編譯過的!以 VC 來說,必須要要在 CTest 這個 function object 的類別裡,額外去指定這個 function object 會回傳的型別,也就是要特別去定義 result_type 的型別才行!修改方法則如同下面的程式碼,也就是黃色的區塊部分。

class CTest
{

public:
int operator()( int a, int b )
{
return a - b;
}

typedef int result_type;
};

而如此修改這個 function object class 的話,使用 bind() 時也就可以簡化,把「<int>」省略掉,寫成下面的形式就可以了:

function< int(int) > func = bind( CTest(), _1, 10 );

這樣的寫法在 gcc 上也是可以正確編譯的。所以如果要考慮到跨平台的話,這有指定 result_type 的寫法,應該會是比較好的。

不過,Heresy 自己不確定這是 TR1 本身的規範?還是 VC TR1 實作上的問題?因為像在《C Standard Library Extensions》一書中,function object 的 class 似乎也都是會加上這行定義的。但是總之,如果要在 VC10 上使用的話,是需要這樣寫的。

而在 Heresy 來看,這樣的需求其實某種程度上應該也限制了一些 function object 的自由;因為一個類別只有一個 result_type,這也代表如果在同一個類別實作了多個 operator() 的話,那不同的 operator() 也都需要回傳相同的型別才行。當然,一般可能不會把程式寫成這樣,不過 Heresy 還是覺得要額外定義 result_type 是比較麻煩的。如果想要避掉這個問題的話,其實可以考慮把 operator() 當作一般的成員函式,用下面的方法來做。

TR1 bind() 用於類別的成員函式

除了上面兩種寫法的,TR1 的 bind() 也可以用在一個物件的成員函式(member function)上的,把一個物件的成員函式轉換成 function object 來用的∼像如果有一個 CTEST2 的類別如下:

class CTEST2
{

public:
int x;

int GetValue( int y )
{
return x * y;
}
};

那可以透過 TR1 bind() 來把他轉換為 function object:

CTEST2 tf;
tf.x = 10;
function<
int(int) > ftestx = bind( &CTEST2::GetValue, &tf, _1 );

這邊可以看到,傳入 bind() 的第一個參數是「&CTEST2::GetValue」,也就是類別 CTEST2 的成員函式 GetValue();不過由於這樣傳進去的話,會不知道是要執行哪個物件的 GetValue() 函式(因為 GetValue() 不是 static 函式),所以第二個參數就是要傳入是要執行哪個物件的 GetValue()。在這邊,就是把我們宣告出來的 tf 這個變數的位址傳進去,讓他去呼叫。而接下來的,則就是像之前 bind() 的 placeholder 的用法一樣,開始指定要 bind 的參數了∼不過這邊 Heresy 是沒有 bind 任何參數。

在這樣寫了之後,呼叫新的這個 function object ftestx 的話,基本上就是和呼叫 tf.GetValue() 完全相同,同時,也可以當作一般的 function object,直接拿來給 STL 的演算法使用了。

mem_fn() 和 ref()

除了 function 這個 template class 和 bind() 這系列的功能外,TR1 其實也還有不少搭配 function object 使用的東西;這邊就稍微再提一下 mem_fn()ref() 以及 cref() 這幾個函式。

首先,mem_fn() 是一個簡單的 wapper,他可以把一個類別的成員函式,轉換為一般的 function object,基本上和上面 bind() 最後一個範例很類似;主要的差異在於 mem_fn() 沒有提供函式參數的處理功能、以及 object 本身要在執行時當作第一個參數傳入而已。下面是一個簡單的範例:

function<int(CTEST2,int)> ftestx1 = mem_fn( &CTEST2::GetValue );
cout << ftestx1( tf, 10 ) << endl;

在上面的例子裡,CTEST2 以及 tf 和前面 bind() 最後一個範例是相同的。而如果只是要把一個類別的函式轉成 function object 的型式的話,是可以考慮使用 mem_fn() 的。此外,mem_fn() 也可以用來取代 STL 的 mem_fun_ref() 來使用。

ref() 這個函式,它的用途是產生一個「reference_wrapper」的物件,可以將一個變數的參考以物件形式來傳遞,一般都是搭配 bind() 來使用。下面就是一個例子:

vector<int> vec;
for( int i = 0; i < 10; i )
vec.push_back( i );


int limit_value = 5;
function<
bool(int)> fG = bind( greater<int>(), _1, limit_value );
function<
bool(int)> fGr = bind( greater<int>(), _1, ref( limit_value ) );
limit_value = 3;

cout <<
"fG:  " << count_if( vec.begin(), vec.end(), fG ) << endl;
cout <<
"fGr: " << count_if( vec.begin(), vec.end(), fGr ) << endl;

在這個例子裡,先建立了一個 int 的 vector,裡面的值是 0 到 9。而接下來,則是夠過 bind() 和 STL 的 greater<int>,產生出兩個 funciton object:fGfGr;其中,兩者的差異僅在於 fG 是將第二個參數的值指定為 limit_valuefGr 則是將第二個參數指定為 ref( limit_value )。而這樣的寫法,都是產生出一元函式,用來判斷所給的值是否大於指定的值(limit_valueref( limit_value ))。

不過這樣實際使用上的差異,在於前者是會將 limit_value 複製一份儲存下來,也就是之後就算修改 limit_value 的值,也不會影響到 fG 的判斷條件的;後者則是會使用 limit_value 的參考,也就是如果之後又在外部修改了 limit_value 的值的話,fGr 的判斷條件也會跟著變動∼

所以像上面這樣的程式,在最後執行 count_if() 的時候,fG 是去判斷「值是否大於 5」,而 fGr 則是去判斷「值是否大於 3」∼所以最後輸出的結果,就會是「fG: 4 fGr: 6」了∼

cref() 的話,則是 ref() 的 const 版本,在這邊就不多加介紹了∼

補充
  1. 如果編譯器有支援,也願意使用 C 0x 的語法的話,其實 bind() 的功能應該是可以完全用 Lambs Expression 來取代掉了∼真的用 Lambda expression 來寫的話,其實某種程度上應該會更方便、好用的。
  2. 感覺上 TR1 的完整參考資料在網路上似乎比較不好找,建議可以參考 Boost C Libraries 版本的 TR1 相關函式庫說明,或是《C Standard Library Extensions》這本書(ISBN:0-321-41299-0),就是專門在講 TR1 的。在 Heresy 來看,這兩者算是相對比較完整的。
  3. Boost C Libraries 相關說明:
    1. function: http://www.boost.org/doc/libs/1_44_0/doc/html/function.html
    2. bind: http://www.boost.org/doc/libs/1_44_0/libs/bind/bind.html
    3. ref: http://www.boost.org/doc/libs/1_44_0/doc/html/ref.html
    4. mem_fn: http://www.boost.org/doc/libs/1_44_0/libs/bind/mem_fn.html

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

  1. boost::function<void()> 可以吃 return type 不是 void 的 function寫 threadpool 的時候發現的

  2. 這個應該是自動轉型了∼實際上,像是 function<int(float)> 也可以拿來接 int(int) 這類型的函式。理論上,應該是可以自動轉型的,都可以自動處理掉。

  3. boost::function<void()> 連 int() 都吃, std::function<void()> 不吃

  4. 的確,試了一下,VC10 不能用 function<void()> 來接 function<int()>,不過在 gcc 裡倒是可以。這應該是 VC 實作的問題了。

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

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