1. 程式人生 > >C++ 下 Function 物件的實現(上)

C++ 下 Function 物件的實現(上)

目錄:

起因在上一篇已經說過了。現在讓我們直接進入主題。本文的目標是,讓以下程式碼能順利跑起來:

int intfun0()
{
    return 1;
}

struct _intfunctor0
{
    int operator()()
    {
        return 2;
    }

} intfunctor0;

struct Test
{
    int intmem0()
    {
        return 3;
    }

} test;

int main()
{
    Function<int ()> f1(&intfun0);
    Function<int ()> f1_(intfun0);
    Function<int ()> f2(intfunctor0);
    Function<int ()> f3(&test, &Test::intmem0);

    f1();
    f1_();
    f2();
    f3();

    return 0;
}

除了上述例子中顯示的,還要支援有返回值的函式和沒返回值的函式,以及有0個、1個、2個、……、MAX 個引數的函式,引數型別無限制。最後實現的 Function 物件僅僅可以執行就好。(至於是否可拷貝、是否可判斷相等 等問題,都是小事,本文暫不考慮。)最後,Bind 概念也不在本文討論範圍之內。

對於這個問題,我們一開始考慮的可能是怎樣統一三種不同形式。有兩個選擇,第一,使用 C++ 的多型機制,最後統一到基類指標的型別;第二,允許類內部有冗餘變數以及必要的 Flag,用於判斷是哪種形式的函式,要如何執行。這樣看起來,第一種方案比第二種爽一點。於是,最初想到的實現有可能是這樣的:

先定義一個虛基類:

template <typename R>
class FunctionBase0
{
public:
    virtual R Invoke() = 0;
    virtual ~FunctionBase0() {}
};

然後實現一個普通函式/仿函式的版本:

template <typename R, typename T>
class Function0 : public FunctionBase0<R>
{
public:
    R Invoke()
    {
        return m_Fun();
    }

public:
    Function0(const T &fun)
        : m_Fun(fun)
    {

    }

private:
    T m_Fun;
};

這裡需要說明的是,如果是普通函式,T會被特化成 R() 或者 R (&)() 或者 R(*)(),取決於使用的時候傳入 fun 還是傳入 &fun。所以不必另外實現針對 R(*)() 的版本。Loki (姑且就以作品名稱乎 Loki 的作者吧,他那個真名實在是太長)在他的書中稱之為“做一個,送一個”。不過對於他書中所說的,我有一個疑惑。Loki 說傳入 fun,模版引數 T 會被特化成 R (&)(),於是一切順利。可是我在操作過程中發現 T 一直被特化成 R (),於是上述 class 中的 m_Fun 被認為是成員函式而不是成員變數。不知道是為什麼,有知道者請不吝指教哈。因為以上原因,本文中我一直用 &fun 的形式對待普通函式。

再實現一個成員函式的版本:

template <typename R, typename T>
class MemberFunction0 : public FunctionBase0<R>
{
public:
    R Invoke()
    {
        return (m_pObj->*m_pMemFun)();
    }

public:
    MemberFunction0(T *pObj, R (T::*pMemFun)())
        : m_pObj(pObj), m_pMemFun(pMemFun)
    {

    }

private:
    R (T::*m_pMemFun)();
    T *m_pObj;
};

最後是一個包裝類。如果你可以接受 Function<int> 表示 int(), Function<int, int> 表示 int (int),…,那麼這裡沒有多少技巧可言。boost 的那個 function 使用的是函式簽名作為模版引數,即 Function<int()>,Function<int (int)> 等形式。如果不太研究語法,可能會像我一樣,一開始會對尖括號裡的 int (int) 之類的玩意兒不太熟悉,覺得很牛逼。可是瞭解了以後,不過是個函式型別而已,沒什麼大不了的。Loki 的 Functor 的使用方式是 Functor<int, TYPELIST_0()>,Functor<int, TYPELIST_1(int)>。其中第一個模版引數始終是返回值,第二個模版引數是引數型別列表,Loki 使用了他創造的玩意兒 TypeList 使得所有函式引數只佔一個坑,這在等下的支援多引數的擴充套件中能夠帶來一些美觀。我比較喜歡 boost 的使用方式,讓使用者直接以語言規定的形式填入函式簽名,而不是一些額外的約定(“第一個模版引數表示返回值”,“第二個到最後的模版引數表示引數”,“第二個模版引數以 TypeList 形式表示函式引數”等)。

為了達到這個目標,我們要玩一些偏特化技巧。關於偏特化,我一直以來的膚淺認識都是錯誤的。我原以為,對於模版類:

template <typename T0, typename T1>
class Foo;

我如果特化其中一個引數 T1:

template <typename T0>
class Foo<T0, int>
{

}

我以為只有這樣才叫偏特化,以為偏特化的過程總是減少模版引數的。而實際上,只要用某個/些型別佔據原始模版引數的位置,就可以了。比如,對於上述 Foo,我可以特化一個 class<T0, std::map<U0, U1>>,消去一個 T1,而新增 U0、U1:

template <typename T0, typename U0, typename U1>
class Foo<T0, std::map<U0, U1>>
{

}

原來 T1 的位置被 std::map<U0, U1> 佔據了,這也是偏特化。當然最後的模版引數數量也可以不變,如:

template <typename T0, typename U>
class Foo<T0, std::vector<U>>
{

}

以及

template <typename T0, typename U>
class Foo<T0, U*>
{

}

其中後者是實現型別萃取的主要方式。只要特化以後,這個類依然帶有至少一個模版引數,就是偏特化。如果最後產生了 template<> 的形式,那就是完全特化。

回到我們剛才的主題,我們要提供給使用者的是這樣一個類:

template <typename Signature>
class Function;

其中引數 Signature 會被實際的函式型別所特化。但是我們只知道整體的一個 Signature 並沒有用,我們必須知道被分解開來的返回值型別、引數型別。於是,引入一個偏特化版本:

template <typename R>
class Function<R ()>

這裡使用 R () 特化原始的 Signature,引入一個新的引數 R。於是返回值型別 R 就被萃取出來了。實現如下:

template <typename R>
class Function<R ()>
{
public:
    template <typename T>
    Function(const T &fun)
        : m_pFunBase(new Function0<R, T>(fun))
    {
       
    }

    template <typename T>
    Function(T *pObj, R (T::*pMemFun)())
        : m_pFunBase(new MemberFunction0<R, T>(pObj, pMemFun))
    {

    }

    ~Function()
    {
        delete m_pFunBase;
    }

    R operator ()()
    {
        return m_pFunBase->Invoke();
    }

private:
    FunctionBase0<R> *m_pFunBase;
};

如果對上面說的“普通函式的使用方式必須是函式指標而不是函式本身”耿耿於懷,可以再引入一個的建構函式:

typedef R (FunctionType)();

Function(const FunctionType &fun)
    : m_pFunBase(new Function0<R, FunctionType &>(fun))
{

}

這裡 FunctionType 是 R(&)() 型別,強制使用它來特化 Function0 中的 T。該建構函式在過載決議中會取得優先權從而使普通函式本身的傳入成為可能。不過,以函式本身形式傳入的普通函式會喪失一些特性,比如 Function<int()> 只能接受 int() 型別的普通函式而不能接受 char () 型的普通函式,因為這種情況下不會走我們剛才新定義的建構函式。

還有一種做法,就是針對全域性函式,強制特化出模版引數為其引用型別的類。定義如下元函式:

template <typename Signature>
struct FunctionTraits
{
    typedef Signature ParamType;
};
   
template <typename RetType>
struct FunctionTraits<RetType ()>
{
    typedef RetType (&ParamType)();
};

然後建構函式改為:

    template <typename T>
    Function(const T &fun)
        : m_pFunBase(new Function0<R, typename FunctionTraits<T>::ParamType>(fun))
    {
       
    }

用以上方法,所有的特性都不會丟失。

到這兒,我們的 Function 已經可以小試牛刀了:

Function<int ()> f1(&intfun0);

Function<int ()> f1_(intfun0);
Function<int ()> f2(intfunctor0);
Function<int ()> f3(&test, &Test::intmem0);

f1();
f1_();
f2();
f3();

上面這段程式碼已經能夠正常運行了。

來,繼續做一個,送一個。下面的程式碼居然也能跑(voidfun0、voidfunctor0、Test::voidmem0類似int版本定義):

Function<void ()> f4(&voidfun0);
Function<void ()> f4_(voidfun0);
Function<void ()> f5(voidfunctor0);
Function<void ()> f6(&test, &Test::voidmem0);

f4();
f4_();
f5();
f6();

這說明了,在類裡面寫一個返回值為該型別的函式,並在裡面寫下 return XXX; 然後以 void 為模版引數傳入該模版類,是符合語法的。驗證一下:

template <typename T>
class Foo
{
public:
    T Bar()
    {
        printf("%s invoked\n", __FUNCTION__);
        return T();
    }
};

int main()
{
    Foo<void> f1;
    f1.Bar();

    Foo<int> f2;
    int i = f2.Bar();

    return 0;
}

執行結果:

Foo<void>::Bar invoked
Foo<int>::Bar invoked

到此為止,我們已經實現了 0 個引數的函式支援,也即 R () 型別的所有函式的支援。接下來還要實現對具有 1 個、2 個、3 個直至任意有限個引數的函式支援。也許您也發現了,接下來的工作可以是體力活,我們可以照葫蘆畫瓢,搞出一堆 FunctionBaseN、FunctionN、MemberFunctionN,並在最後的 Function 中再實現 N 個偏特化版本。是,不錯,大致上原理就是這樣。限於篇幅,我想暫時寫到這裡,下篇將繼續談談巨集、TypeList,以及怎樣少花點力氣實現其餘 N 個版本。最終達到的效果是,只要改一個巨集定義,就可以提高參數上限。

在本文所涉及的內容中,我比較糾結的是,可否在不用多型機制的情況下達到比較優雅的形式統一?

歡迎討論。

posted on 2011-01-16 22:17 溪流 閱讀(6103) 評論(55)  編輯 收藏 引用 所屬分類: C++