C++:關於委託類

qinfengxiaoyue發表於2014-05-14

轉自:http://blog.csdn.net/dadalan/article/details/4041931。vs2010已經支援function/bind,能很好實現委託。

[說明]

     本文不僅介紹了C++語言應用非常好的一種方法(我甚至覺得應該將它歸結為一種設計模式),而且也是對C#語言中委託特性底層實現的一個很好的說明。

        閱讀本文,你應當對委託的概念有所瞭解;在討論委託是實現時,你應當對標準模板庫(STL)中的list容器以及迭代器(iterator)有所瞭解。

在這篇文章中,暫不討論類成員函式。

 

1.C#中的委託

    你如果對C#語言比較瞭解的話,就應該會知道C#語言中有一個很好的特性,那就是委託。它能夠大大簡化在某些特定的場合呼叫多個相同形式函式的處理。特別是在像Windows程式中,用委託響應訊息十分方便。舉一個常見的例子。現在的Windows應用程式框架都比較複雜,一個應用程式可能由許多部件組成,很多時候它們都需要響應同一個訊息。如在一個MDI中,多個子窗體都要響應主窗體的WM_QUIT訊息。很多時候,我們並不需要像MFC那樣將所有的處理都封裝在類中,我們需要一種簡單易用的方法,直觀地解決這個問題。這篇文章為你提供了這樣的方法。

    在具體介紹這篇文章之前,讓我們先看看在C#中是怎樣使用委託的。首先,在像處理Windows訊息這樣的操作時,被通知的物件都以一種固定的格式來接受訊息。在C#中,處理訊息的委託被定義為如下格式:

    public delegate void WinEventHandler(object sender, EventArgs arg);

假設在一個MDI中,主窗體由MainFrame實現。我們在主窗體定義一個專門用於處理WM_QUIT訊息:

    public event WinEventHandler OnQuit;

在上面的宣告中,public表明該委託可以在外部訪問(註冊/刪除),event關鍵字表明這是一個事件,只能在類內部呼叫,外部不能直接觸發它。在MainFrame的窗體過程函式中,WM_QUIT訊息是這樣被分發的:

protected virtual void WndProc(Message msg,object sender,EventArgs arg)

{

       ......

       switch(msg)

       {

              ......

       case WM_QUIT:

              OnQuit(sender,arg);

              break;

       ......

       }

}

那麼,MainFrame的子窗體如何響應主窗體的WM_QUIT訊息呢?首先,你要實現子窗體Child1處理該訊息的函式,它的宣告形式要跟WinEventHandler委託相同:

//In Child1 Class

protected void Child1_On_Quit(object sd, EventArgs ags)

{

       this.SaveAll();        file://做些善後工作,如儲存當前資訊等

       ……

}

在子窗體被初始化時向MainFrame的OnQuit委託註冊這個函式。如果MainFrame是Child1的父窗體,那麼其實現可能是這樣的:

       parent.OnQuit += new WinEventHandler(this.Child1_On_Quit);

這樣,當MainFrame收到WM_QUIT訊息時,呼叫OnQuit委託,同時Child1.Child1_On_Quit也被呼叫,從而實現訊息傳遞。當然,也有子窗體不再需要響應主窗體的WM_QUIT訊息的時候。我們可以通過下面的方式從MainFrame的OnQuit委託中登出它:

    parent.OnQuit -= new WinEventHandler(this.Child1_On_Quit);

這一步也是很必要的。如果Child1先於主窗體MainFrame被摧毀,而Child1_On_Quit沒有從MainFrame.OnQuit委託中登出,則主窗體收到WM_QUIT訊息時呼叫OnQuit委託,它又順序呼叫到Child1.Child1_On_Quit,則可能引發空引用異常了。[詳細介紹,請參見《IL程式碼底層執行機制:函式相關]

       委託可以接受多個例項方法,因此你可以向一個委託註冊多個方法。實際上,委託包含了一個方法引用的列表,當委託被呼叫時,它將順序呼叫其列表中的方法引用。這一點我還會在後面詳細說明。

 

2.在C++中實現委託

       我們知道了C#中委託的原理,是不是也可以在C++中實現呢?答案是肯定的。不同的是C#在語言級別提供了對委託的支援,而C++沒有,它需要我們對委託進行定製。這樣,每種不同形式的委託都要有不同的實現,靈活性大打折扣。幸運的是,作者已經提供了一個名為delegate.exe的實用小工具,它可以幫我們實現由委託宣告生成實際程式碼。其用法將在後面詳細介紹。

       現在,我們主要考慮的是如何來實現我們自己定製的委託。前面我已經簡單介紹了一個委託應當具備的因素:儲存方法引用(在C++中是指標,但在這裡我還是習慣稱之為引用)的列表,新增/刪除方法引用,以及最重要的呼叫例程。有多中方法可以實現這些操作,這裡我們採用類來實現。

       第一個要考慮的是如何來儲存方法引用。因為方法引用(指標)實際上是一個32位無符號整數,因此我也採用無符號整型來儲存方法引用(指標)。這裡,我定義了這種資料型別:typedef unsigned int NativePtr。在接受

       第二是宣告這個委託的形式。在這個例子中,我採用void Handle(char *str)的形式作為示例。我們定義這種函式指標型別typedef void (* Handler)(char *str)以供函式呼叫時,作為由無符號整形向函式指標的轉換型別。

       第三個要考慮的是怎樣實現多個方法引用(指標)的儲存。最簡單的方法是使用STL中的list列表容器儲存。list模板類為我們提供了一組非常方便的列表存取操作方法,它提供的迭代器使我們能夠很容易地使用它。在CDelegate類中,我定義了用於儲存函式引用的欄位ftns:list <NativePtr> ftns.

       第四是實現新增/刪除函式引用。這裡兩個操作分別由AddFunction/RemoveFunction來實現。在這裡,有一個問題是我們接受什麼樣的引數型別,怎樣接受。毫無疑問,它要接受的是前面定義的Handler型別。但我們已使用32為無符號整型來儲存其資訊,因此,為了簡單起見,AddFunction/RemoveFunction函式的引數為void * 型別,這樣它可以接受許多型別的引數。關於使用什麼樣的型別作為AddFunction/RemoveFunction的引數這一點,我想可能還要詳細討論一下,究竟是前面定義的Handler型別還是void *型別。應該說兩種型別都有其優缺點,關鍵是看我們在什麼時候應用。當然,使用我們定製的委託型別(也即前面定義的Handler型別)作為其引數型別可以讓編譯器為我們做必要的語法檢查,以防止不匹配的引數被傳替。這就要看實際情況了。

當然,過載 += 和 - = 操作符是必須的了。

       最後,也是最重要的是實現我們的Invoke方法。我們對Invoke方法有要求,它必須和我們定製的委託型別是一致的。同時,我們也需要過載()操作符號,以方便我們像一般函式那樣呼叫它。

       下面我給出類的定義部分:

#include <list>

using namespace std;

/*定義一個無符號型32位整數型別,該型別用於儲存函式引用(指標)*/

typedef unsigned int NativePtr;

/*定義函式原型,返回值須為空,引數根據需要可改變*/

typedef void (* Handler)(char *);

class CDelegate

{

private:

       /* 函式列表,被新增的函式引用(指標)都放在該列表中 */

       list<NativePtr> ftns;

public:

       /*新增函式引用(指標)*/

       void AddFunction(void *);    

       /*刪除函式引用(指標)*/

       void RemoveFunction(void *);

       /*呼叫例程:最重要的部分,實現對列表中的函式逐個呼叫*/

       int Invoke(char *);

       /*運算子過載AddFunction方法*/

       void operator += (void *);

       /*運算子過載RemoveFunction方法*/

       void operator -= (void *);

       /*運算子過載Invoke方法*/

       int operator ()(char *);

};

下面我給出各個方法實現的程式碼。

#include "Delegate.h"

void CDelegate::AddFunction(void *ftn)

{

       NativePtr np=(NativePtr)ftn;

       ftns.push_back(np);

}

AddFunction函式接受型別為void * 的引數,然後將這個引數強制轉換為NativePtr(unsigned int)型別,存放於ftns列表中。注意,這個數值從列表的尾部插入,以實現FIFO。

void CDelegate::RemoveFunction(void *ftn)

{

       NativePtr np=(NativePtr)ftn;

       ftns.remove(np);

}

RemoveFunction函式接受型別為void * 的引數,然後將這個引數強制轉換為NativePtr ( unsigned int ) 型別,再從ftns中刪除與它的值相同的元素。

void CDelegate::operator += (void *ftn)

{

       this->AddFunction(ftn);

}

+=操作符過載AddFunction方法。

void CDelegate::operator -= (void *ftn)

{

       this->RemoveFunction(ftn);

}

-=操作符RemoveFunction方法。

int CDelegate::Invoke(char * pch)

{

       Handler handle;

       list<NativePtr>::iterator itr=ftns.begin();

       try

       {

              for(;itr!=ftns.end();itr++)

              {

                     handle=(Handler)*itr;

                     handle(pch);

              }

       }

       catch(char *)

       {

              return 0;

       }

       return 1;

}

使用list模板類提供的迭代器,遍歷ftns中的每個元素,順次將元素轉化為定製的函式引用(指標)型別,並呼叫其所對於的函式。這裡要求委託返回值必須為空。如有異常,則Invoke返回0值。

int CDelegate::operator ()(char *pch)

{

       return Invoke(pch);

}

()操作符過載Invoke方法。

可以看到,我們實現的這個委託類其實很簡單。將新增的函式引用(指標)新增到一個列表中;當委託被呼叫時,將列表中的函式引用逐個取出並呼叫。C#中的委託的實現也是如此;它對委託的處理,程式生成器的腳色是由編譯器扮演的。其實如果你對由C#編譯器生成的IL程式碼進行剖析,每個C#委託宣告也都是被轉化為繼承自某個支援類似功能的類處理的。同時,也正是由於委託管理著多個方法的呼叫,它不能處理它們的返回值,所以委託要求被委託的函式不能具有返回值。

 

下面是執行示例:

#include "Delegate.h"

#include <iostream>

#include <windows.h>

 

void Say1(char *s)

{

       cout<<"In Function Say1:   ";

       cout<<s<<endl;

}

void Say2(char *s)

{

       cout<<"In Function Say2:   ";

       cout<<s<<endl;

}

void STHeoaie(char *s)

{

       MessageBox(NULL,s,"Delegate",MB_OK);

}

void main()

{

       CDelegate dlg;

       dlg.AddFunction(Say1);

       dlg.AddFunction(Say2);

       dlg+=STHeoaie;

       int rs=dlg.Invoke("Hello,World!");

       if(!rs) cout<<"Failed."<<endl;

/*

       第一次呼叫結果:       

      

*/

       dlg-=Say2;

       rs=dlg("The second invoking by CDelegate!");

       file://等同於dlg. Invoke("The second invoking by CDelegate!")

       if(!rs) cout<<"Failed."<<endl;

/*

       第二次呼叫:

 

   */   

       dlg-=Say1;

       dlg-=STHeoaie;

       rs=dlg.Invoke("The Third invoking by CDelegate!");

       if(!rs) cout<<"Failed."<<endl;

/*

       第三次呼叫,沒有任何輸出,因為已登出所以方法:

 

*/

}

 

3.關於實用小工具delegate.exe

       為了解決在定製委託時的不靈活性,我特意編寫了這個小工具,它能夠方便地將委託宣告轉化為如上面所述的程式碼。下面是其基本用法。

       在你的某個標頭檔案中,如test.h,以__delegate 關鍵字宣告一個委託:

       __delegate void WinHandler ( HWND hwnd ,UINT message ,WPARAM wParam,LPARAM lParam);

然後轉到命令列模式,進入test.h的目錄,鍵如如下命令:

       delegate.exe test.h /out test.hxx

它將生成test.hxx檔案。你可以向你的源程式中包含這個檔案,以使用你所定義的委託。如,可以是這樣:#include “test.hxx”

       你可以在一個檔案裡定義多個委託,也可以在多個檔案裡定義多個委託.但是你只能指定一個的輸出檔案.如果你沒有用/out 選項指定輸出檔案,則預設輸出為delegate.h。如:

       delegate.exe test.h test1.h test2.h /out test.hxx

使用/help選項得到幫助資訊,使用/version選項得到版本資訊.

 

注意:最新的Visual C++ .Net 版本已經支援同名關鍵字 __delegate,這是微軟公司為了將Visual C++向.net移植而新增的新關鍵字,只有 Visual C++ .Net 支援,其他如Visual C++ 6.0、Borland C++ Builder 、GNU C++ 等都不支援。但兩個完全沒有聯絡.幸運的是,delegate小工具支援/keyword 選項,它可以指定你自己定義的關鍵字,如__delegate__。

相關文章