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


在前一篇的《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. function 上也包含自動轉型,只要型別有辦法自動轉換,應該就可以用;例如 function< int(float) > 可以用 function< float(double) > 來接。不過在 VC10 裡,回傳值的型別沒辦法用 void 來取代;例如 function< int(float) > 想用 function< void(float) > 來接的話,會產生編譯錯誤,但是這在 gcc 是可以正常編譯過的。
  2. 如果編譯器有支援,也願意使用 C++0x 的語法的話,其實 bind() 的功能應該是可以完全用 Lambs Expression 來取代掉了~真的用 Lambda expression 來寫的話,其實某種程度上應該會更方便、好用的。
  3. 感覺上 TR1 的完整參考資料在網路上似乎比較不好找,建議可以參考 Boost C++ Libraries 版本的 TR1 相關函式庫說明,或是《C++ Standard Library Extensions》這本書(ISBN:0-321-41299-0),就是專門在講 TR1 的。在 Heresy 來看,這兩者算是相對比較完整的。
  4. 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

Part 1. Function Pointer
Part 2. Function Object

廣告

關於 Heresy
https://kheresy.wordpress.com

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

  1. 引用通告: Boost 與 Qt 的 Signal / Slot 效能測試 | Heresy's Space

  2. 引用通告: 跨平台的 plugin 開發函式庫:Boost DLL – 基本使用 | Heresy's Space

  3. 引用通告: 體感按鈕實作(OpenCV) | Heresy's Space

  4. 引用通告: C++0x:Lambda expression « Heresy's Space

  5. 引用通告: 在 C++ 裡傳遞、儲存函式 Part 2:Function Object « Heresy's Space

  6. 引用通告: 在 C++ 裡傳遞、儲存函式 Part 1:Function Pointer « Heresy's Space

  7. 引用通告: Boost 的事件管理架構:Signal / Slot(下) « Heresy's Space

  8. 引用通告: Boost 的事件管理架構:Signal / Slot(上) « Heresy's Space

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com Logo

您的留言將使用 WordPress.com 帳號。 登出 / 變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 / 變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 / 變更 )

Google+ photo

您的留言將使用 Google+ 帳號。 登出 / 變更 )

連結到 %s

%d 位部落客按了讚: