2014年1月8日

[C++] Functor (仿函式) - A Function-Like Object PART II - The Use of Functor

PART I 時我們簡單的介紹了下 qsort,並且說明了在結合 OO 時將會遇上一些問題,現在就讓我們來看看會遇上怎樣的問題吧!




首先,老樣子先看一個範例:


class A
{
private:
    int value[10];
    int compare(const void* lhs, const void* rhs);

public:
    A();
    void sorting();
};

A::A()
{
    for(int i=0; i<5; ++i) value[i] = i << 1;
    for(int i=5; i<10; ++i) value[i] = ((i-5) << 1) + 1;
}

int A::compare(const void* lhs, const void* rhs)
{
    return (*(int*)lhs - *(int*)rhs);
}

void A::sorting()
{
    qsort(value, 10, sizeof(int), this->compare());
}

int main()
{
    A a;
    a.sorting();
    return 0;
}

假設 A 是為了某種目的所設計的 class,那這邊為了簡單起見,假設他只有一個 private member value,並且 value 是個陣列,亦是我們所想要排序的目標。而因為它是 A 的 private member,所以我們提供另外兩個 member function compare 與 sorting,前者是提供 sorting 所需的 compare function,後者是供使用者呼叫所提供的介面。當然,sorting 這個 function 也可以不必是 member function,這時只要把 compare 的存取權限改成 public 即可,只是無論如何 compare 非得是 member function,不然無法存取 private member value

好的,問題來了,把這段 code 送去 compile 會發現不管怎樣就是無法通過編譯,而 compiler 回報錯誤的地方就是那個 compare!其原因很簡單,C++ class 的 member function 其機制與 C 的 function 不同,因此其內部實做上也有差異,自然存取方法上也就不同。基於這種差異,我們沒辦法讓 compare function 是一個 class 的 member function,compare function 只能是 global function 或是一個 class 的 static member function。如此一來我們其中一種解法:把 compare function 宣告成 A 的 static member function。不過接下來我要描述的這情況會讓這種解法同樣失效,考慮我們所要排序的 value 存的其實只是某個真正存資料的 array 的 index,換言之 compare function 要改寫成:


static int A::compare(const void* lhs, const void* rhs)
{
    return (data[*(int*)lhs] - data[*(int*)rhs]);
}

假設 data 是真正存資料的 array,他同時也是 A 的 private member,在 compare 是 A 的 static member function 時,我們必須讓 data 也是 A 的 static member data,很明顯得這難以滿足大多數情況,因此讓 compare function 成為 class 的 static member function 這條路也行不通了。剩下的另一條路就是讓 compare function 變成 global function,因此在當時我有想出一個以破壞封裝性為代價的解法:讓 A 的 member 變成 public 或是用一個 global variable 去抓出我要 access 的資料。很明顯的,這兩種解法都很糟,所以這邊就不多加著墨了,讓我們來看 functor 能在此時為我們提供怎樣的解法。

首先我們先來介紹一個 C++ algorithm 這個函式庫所提供的另一個 sorting function:std::sort。用法很簡單,以 PART I 的第一個例子來看的話,要排序 value 那個陣列只要這樣寫:


bool compare(const int lhs, const int rhs)
{
    return (lhs < rhs);
}

std::sort(value, value+sizeof(value)/sizeof(int), compare);

第一個參數是起始位置,第二個參數是結尾後面的位置,第三個參數一樣是 compare function,不過稍為有點不同的地方是 compare function 的回傳值是 bool,目的是告訴 std::sort 丟給 compare function 的第一個參數是否要放在第二個參數前面。這邊有趣的地方就在這第三個參數能填的東西不僅僅只有 function,他也可以是個 object,精確來說,第三個參數不管你丟什麼東西進去,只要他具有 function 功能即可!所以即便今天第三個參數丟進去的是個 object,只要 std::sort 可以像是呼叫 function 般的來使用,那他才不管丟進來的東西的 "型態" 到底是什麼,而這點就是讓 functor 可以在此處派上用場的原因。來看個範例:

class Comparer{
private:
    const int* const target;
	
public:
    Comparer(const int* const d){this->target = d;}
    bool operator()(const int a, const int b)
    {
        return target[a] < target[b];
    }
};

void A::sorting()
{
    sort(value, value+10, Comparer(data));
}

首先我們寫一個 class 叫 Comparer 用以取代 compare function,但因為必須要能讓這個 class 的 object 具有 function 功能,所以我們 overload operator (),因此由 Compare 產生的 object 即為具有 function 功能的 object,也就是 functor。這個 functor 要做的事情很簡單,判斷 std::sort 丟給他的兩個數字,判斷誰要排在誰前面,但這邊因為我們要比較的對象並非丟給 Comparer 的那兩個數字本身,而是以他們做為 index 去存取另一個陣列的資料才是真正要比較的對象。所以 Comparer 有一個 private member target,並且在 contructor 那邊接受一個參數,由這個參數來得知所要比較的 "真正" 對象到底是誰。因此在 std::sort 的第三個參數我們改為傳 Comparer(data) 這個 functor,讓 std::sort 可以得知正確的比較結果。同時這個做法也無須去更改 class A 的設計,也比較沒有封裝性上的問題 (因為要暴露多少資料可以由 class A 自己來決定)。所以相較於前面的做法,這個解法明顯得好多了。

functor 有用的地方就在於他本身其實是個 object,所以他比 function 多了記錄資料的功能,這個能力促使我們更加靈活與方便的撰寫程式而不會受限於 function 的規格。善加利用這個技巧,我們就可以在 OO 的設計上有更多的選擇,


Appendix
在 C++11 引入 lambda expression 後,這邊其實還有另外一種做法可以參考:

void A::sorting()
{
    auto compare = [&](const int lhs, const int rhs) -> bool {
        return (data[lhs] < data[rhs]);
    }
    sort(value, value+10, compare);
}

以我個人來說,我更喜歡這種做法,因為在這種情況下他可以不用多寫一個用途明顯受限的 class,而且小巧精簡,程式撰寫與閱讀上也更加方便。不過現在仍有許多 compiler 不支援新標準,所以可攜性上是個比較大的問題,但若 compiler 支援,我認為這是個值得花時間好好研究的對象。

Reference
[1] https://gist.github.com/CrBoy/719111

Acknowledgement
感謝當時指引明路的畢玉泉,[1] 即為他當時給的範例

沒有留言:

張貼留言