天天看點

在你的C++程式裡實作委托 (轉)

[說明]

  本文不僅介紹了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;

typedef unsigned int NativePtr;

typedef void (* Handler)(char *);

class CDelegate 

{

private:

  list<NativePtr> ftns;

public:

  void AddFunction(void *);  

  void RemoveFunction(void *);

  int Invoke(char *);

  void operator += (void *);

  void operator -= (void *);

  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__。

  由于隻是測試版本,這個小工具的功能還不是很強。比如,你不能在委托聲明語句中間有注釋,而隻能在聲明前或聲明之後添加注釋。另外由于時間關系,在處理委托傳回值沒有做到很好的處理。希望這個問題很快會解決。有什麼意見和建議,請将之回報到郵箱:[email protected].

[1]關于委托的更多資訊,請參見拙作<IL代碼底層運作機制:函數相關>

[2]關于标準模闆庫(STL),請參考相關文檔或書籍,如MSDN或 <STL源代碼剖析>

[3]關于疊代器(iterator),<設計模式>中有詳細論述

[4]關于delegate.exe小工具詳細資訊,請參見附文<委托代碼生成器>