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


前一篇大致介紹了 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 的一些擴充,來做進一步的說明的~


Part 1. Function Pointer
Part 3. Function Object in TR1

對「在 C++ 裡傳遞、儲存函式 Part 2:Function Object」的想法

  1. 請問!一般的函式不能宣告為inline增加執行速度嗎?為什麼要特別強調inline是functor的優點?

    • 一般的 function 當然可以使用 inline 來加速,但是這邊主要是和 function pointer 在做比較。
      因為 function pointer 是沒辦法透過 inline 直接展開的。

發表迴響

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

WordPress.com 標誌

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

Twitter picture

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

Facebook照片

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

連結到 %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.