牵着老婆满街逛

严以律己,宽以待人. 三思而后行.
GMail/GTalk: yanglinbo#google.com;
MSN/Email: tx7do#yahoo.com.cn;
QQ: 3 0 3 3 9 6 9 2 0 .

仿函数、绑定、桥接、委托相关讨论

from:http://www.gmdev.cn/program/index.html

仿函数、绑定、桥接、委托相关讨论:

以下随便讨论下,没突出的中心论点,个中理论只代表我个人观点,难免有错:),欢迎指正。

一。需求:

在事件处理常常会碰到这样的情况:

1。接口分离。即invokers(调用者)与(receivers)接收者分离。

2。时间分离。

比如说:UI相关元素(按钮、菜单等)就是一个invokers。

receivers则是响应命令的对象(如对话框或应用程序本身)。

这需要我们要先将UI相关元素的事件响应的接收者在初始化时先保存起来。

待后用户按下按钮等再触发(即invokers通过调用对应先前保存的receivers来执行命令)

嗯,在delphi、java、vcl、.net中有相关的实现。而vc则需要自己来弄。

二。仿函数的实现:

在说仿函数前先说说我们应该怎么保存这些操作相关函数的呢?

// 一般的函数我们可以这么存:

void (*fun)() = Test;

(*fun)();

// 而类成员函数可以这么做:

void (CTest::*mfn)(); // 或用 typedef void (CTest::*MFN_TEST)(); MFN_TEST mfn;

mfn = CTest::Test;

CTest a, *p=new CTest;

(a.*mfn)(); // 调用方法1

(p->*mfn)(); // 调用方法2

如上所述可见为了处理前面所述的事件响应情况,我们通常会用回调函数,

就是把类成员函数定义为静态函数,在初始时保存函数地址(与一般函数处理类同)及对应的对象指针,

在事件触发时调用对应的静态函数,而该函数中在把指针强制转化为对应类型对象地址,

得以操纵该对象的成员变量(嗯,理论上跟成员函数的实现差不多,成员函数会由编译器安插一个

this指针作为第1个参数传给函数,以便可以操作该this对象的成员)。

回调函数应用的具体代码如下:

1). 回调接口(静态函数法):

//======================================================

#include "stdafx.h"

#include

typedef void(*KEY_RESPOND)(void* /*,param*/);

struct CListener

{

void* pThis;

KEY_RESPOND pfn;

CListener() : pThis(0), pfn(0){}

};

class CInput

{

std::list m_listListener;

public:

void AddListener( CListener* pListener ){

m_listListener.push_back( pListener );

}

void RemoveListerner( CListener* pListener ){

std::list::iterator iter;

for( iter = m_listListener.begin(); iter!= m_listListener.end();iter++ ){

if( pListener == (*iter) ){

m_listListener.erase( iter ); break;

}

}

}

void HitOneKey(){

std::list::iterator iter;

for( iter = m_listListener.begin(); iter!= m_listListener.end();iter++ ){

if( (*iter)->pfn && (*iter)->pThis ){

(*(*iter)->pfn)( (*iter)->pThis );

}

}

}

void clearListener(){

m_listListener.clear();

}

};

class CUI

{

public:

static void InputProc(void* pThis/*,param*/){

__asm int 3 // 下个断点测试下(某些编译器不能这么写,vc可以)

}

};

CUI ui;

CInput input;

int _tmain(int argc, _TCHAR* argv[])

{

// 初始:

CListener* pListener = new CListener;

pListener->pfn = &CUI::InputProc;

pListener->pThis = &ui;

// 触发:

input.AddListener( pListener ); // input即为invokers(调用者,但叫触发者好点)

input.HitOneKey(); // 某处事件触发,内部呼叫receivers(这里是原先保存的CUI对象)来真正处理该事件(InputProc(...)方法)。

// 清除

input.clearListener();

if( pListener )

{

delete pListener;

pListener = NULL;

}

return 0;

}

//======================================================

// 第2种方法: 回调类(虚函数多态法):

//======================================================

class IWillBack

{

public:

virtual void InputProc(/*参数略...*/){}

};

class CInput

{

public:

void RegisterListener(IWillBack* pListener){

m_pListener = pListener; // 这里用list存起来才好,这里只作测试

}

void OnInputEvent(){

m_pListener->InputProc(/*参数略...*/);

}

private:

IWillBack* m_pListener;

};

class CUI : public IWillBack

{

public:

void InputProc(/*参数略...*/){ /*..实际处理代码..*/}

private:

};

int _tmain(int argc, _TCHAR* argv[])

{

CInput aa;

CUI bb;

aa.RegisterListener(&bb);

aa.OnInputEvent();

return 0;

}

//======================================================

但是第1种静态函数用法是不直观的,第2种需要派生增加了之间的联系,而为了方便我们通常会将成员函数指针转化为函数对象来处理,即仿函数(一般是指重载了()操作符的类)来实现。

类似于这样的操作,stl提供了mem_fun、mem_fun_ref、binder1st、binder2nd简单操作。

但stl的方法相对比较原始而受限制,比如说std::mem_fun需要成员函数有返回值,

std::mem_fun最多只能支持成员函数有一个参数等,

下面来看std:mem_fun_ref不支持成员函数返回值为void的一个例子:

//======================================================

#include

class CFoo

{

public:

void test() // 只有将void改成别的类型才可以,如:int

{

return 0;

}

};

void main()

{

CFoo t;

std::mem_fun_ref(&CFoo::test)(t);

}

//======================================================

上述代码只有将void改成别的类型(如int)才可以,

那么为什么不可以处理返回void的函数呢? stl的实现究竟是怎么样的呢?

嗯,stl简单实现了mem_fun_ref及mem_fun,其中mem_fun_ref以引用方式处理函数所属对象,

而mem_fun以指针方式处理函数所属对象。

现在让我们从vc的stl挖出部份代码来看看,

1.stl的实现:

以mem_fun_ref为例(省略某些对说明不重要的细节,两条虚线包括的代码为stl类似源码):

//======================================================

//----------------------------------------------------------

namespace stl_test

{

// 主要实现:

template

class mem_fun_ref_t

{

public:

mem_fun_ref_t( R (T::*_Pm)() ) : _Ptr(_Pm) {} // 构造: 保存成员函数地址

R operator()(T& _X) const // 调用: 这里可看出mem_fun_ref以引用方式处理

{

return ((_X.*_Ptr)()); // 这里执行调用函数,并返回该函数所返回值

}

private:

R (T::*_Ptr)(); // 指向成员函数地址的指针

};

// 这里只是利用函数的参数推导来自动获取型别(方便调用)

template inline

mem_fun_ref_t mem_fun_ref(R (T::*_Pm)())

{

return (mem_fun_ref_t(_Pm));

}

} // end of namespace test_stl

//----------------------------------------------------------

class CFoo

{

public:

int test1(){

__asm int 3

return 0;

}

void test2(){

__asm int 3

}

};

int APIENTRY WinMain(HINSTANCE hInstance,

HINSTANCE hPrevInstance,

LPSTR lpCmdLine,

int nCmdShow)

{

CFoo t;

stl_test::mem_fun_ref( &CFoo::test1 ) (t);

return 0;

}

//======================================================

/////////////////////////////////////////////////////////////////

从源码"return ((_X.*_Ptr)()); " 可以看到stl直接返回该函数所返回值。

所以函数没有返回值(即为void时)的话编译器就会报错。好,那么如果我们在

这里只是直接执行函数而不用return返回的话编译器应该可以通过了。

嗯,boost中正是这么处理的。(btw.为了更为通用,boost对stl原有仿函数及绑定作了大量的改进工作)。

但是具体应该怎么区分有没有返回值呢?这个也容易,我们只需用到模板的偏特性就可

以做到。下面就看看boost的实现(btw.boost有两种版本,我用的是兼容版本,代码难看)

2. boost的实现(这里我把boost的一大堆宏(真@$@#@#难看,loki在这方面来得比较清爽)去掉了):

/////////////////////////////////////////////////////////////////

// notreturn.cpp : Defines the entry point for the application.

//

#include "stdafx.h"

//------------------------------------

namespace boost_test

{

template // 有返回值时会调用这个

struct mf

{

template

class inner_mf0

{

R (T::*_Ptr)();

public:

inner_mf0(R (T::*f)()) : _Ptr(f) {}

R operator()(T& X) const

{

return ((X.*_Ptr)());

}

};

};

template<> // 没有反回值时会调用这个

struct mf // 偏特化

{

template

class inner_mf0

{

R (T::*_Ptr)();

public:

inner_mf0(R (T::*f)()) : _Ptr(f) {}

R operator()(T& X) const

{

((X.*_Ptr)());

}

};

};

// 创建一派生类,派生于上述基类

template

struct mf0 : public mf::inner_mf0

{

typedef R(T::*F)();

explicit mf0(F f) : mf::inner_mf0(f) {}

};

// 通过函数的参数推导自动获取类型

template

mf0 mem_fn( R(T::*f)() )

{

return mf0(f);

}

} // namespace boost_test

//------------------------------------

class CFoo

{

public:

int test1(){ return 0; }

void test2(){}

};

int APIENTRY WinMain(HINSTANCE hInstance,

HINSTANCE hPrevInstance,

LPSTR lpCmdLine,

int nCmdShow)

{

CFoo t;

boost_test::mem_fn( &CFoo::test1 ) (t);

return 0;

}

/////////////////////////////////////////////////////////////////

从上述代码可以看到偏特性帮助我们解决了返回值为void的情况。但是手写了两份

基本相同的代码。。。

另外处理参数个数的情况也很容易,只要分别实现不同参数的各个模板类就可以了,

boost最多只能支持成员函数有8个参数,因为它内部实现了8份这样不同参数模板类。

其实的处理方法都是一模一样的,可是由于语言的限制我们还是没有办法不一一实现

不同参数的类:。在loki中参数可以用TList实现任意的参数,但是在实现还是得老

老实实的每份手写一份(loki实现了15份可以支持15个参数)。

这真让人郁闷。。。不过没办法。

说完来仿函数,下面开始说说有关绑定,stl、boost、loki的绑定的意思是

对某物实体的“绑定”,通俗来说是指对函数、构造函数、仿函数等与其对应的某个参数的绑定,

以便在调用时不用再次输入此参数(因为某些时候参数是固定的,比如说绑定一个内部存有

成员函数地址的仿函数和它对应的对象地址在一起)。

以下是stl的bind用法:

//================================

#include "stdafx.h"

#include // stl

#include // boost

struct CFoo {

int test(int){

return 0;

}

};

void main()

{

boost::function1 f; // 这里用了boost

CFoo obj;

f = std::bind1st(std::mem_fun(&CFoo::test), &obj);

f(5);

}

//================================

loki中的BindFirst比较类似于stl的binder

(binder1st,binder2nd),但是它是通用的,可以通过嵌套实现任意多个参数绑定:

//================================

void f()

{

Functor cmd1(something);

Functor cmd2(BindFirst(cmd1, 10));

cmd2(20);

Functor cmd3(BindFirst(cmd2, 30));

cmd3();

}

而boost中的实现是以占位符来表现,具体如何实现,下回继续讨论(嗯,

boost代码的宏太多了,这部份还是等有空再补全了,现在我们来看看如何实现一个委托类)

三。委托类的实现:

1. 桥接模式:

设计模式告诉我们可以使用桥接模式(Bridge Pattern)减少对象之间的 耦合度,桥接模式如下:

Invoker <>------------------->* Interface

^

|

Receiver

上图的Invoker表示事件触发者,Receiver表示事件处理者,符号类似于<c++大规模编程。。>一书所描述(注:这里符号对位可能出错:变成左对齐了),

其中<>------------------->表示Invoker 内含(拥用)Interface(即Invoker 有Interface的变量或指针并负责Interface的释放),

而*号表示可有多个。

^

| 号则表示继承于(Receiver继承于Interface)。

好,我们先来分析前面在" 第2种方法: 回调类(虚函数多态法):"的实现思想(请回到前面看看代码),

它其实就是一个桥接模式,如下(括号内对应前面所实现的类):

Invoker(CInput) <>--------------->* Interface(IWillBack)

^

|

Receiver(CUI)

对照我们前面实现的代码可以发现此种实现的桥接的缺点是:每一个想要

注册一方法到Invoker中以便Invoker在事件触发时调用的类(如Receiver)都要派生自Interface。

有没有更好的办法再次减少这种耦合度呢?这正是下面我们要讨

论的下一种设计模式:

2. 委托与事件:

委托的处理设计如下:

Invoker <>--------------------->* Interface

^

|

Implementation -----------------> Receiver

即在原桥接模式下再加一层间接性:Implementation 。其中

Implementation与Receiver之间的----------------->表示Implementation引用了Receiver一些服务,

即Implementation用到了Receiver某些东西(如函数或数据)。嗯,这些解释不知是否适当,希望不会误导。。。

好,一开始可能我们会这么设计:

//======================================================================================

class handle {};

template

class Implementation : public handle

{

T* m_pThis;

public:

Implementation ( T* pThis ) : m_pThis(pThis) {}

template

void execute( void (T::*mfn)(T1), T1 arg ) { (m_pThis->*mfn)( arg ); }

};

struct Receive {

void Proc(int) {

__asm int 3

}

};

Receive a;

void Invoker(){

Implementation test = Implementation (&a);

test.execute( Receive::Proc, 10 ); // 当事件发生时调用

};

int _tmain(int argc, _TCHAR* argv[])

{

Invoker();

}

//======================================================================================

但是Invoker知道了太多Receive的信息,况且我们想让触发者Invoker作成一个类。

一个改进的版本如下:

//-------------------------------------------------------------

// signal slot system

// 注: 该法我是看了"落木随风"的"委托、信号和消息反馈的模板实现技术",

// 代码作了部份添加。在这里非常的感谢他!

// 他的博客:http://blogs.gcomputing.com/rocwood/archives/000154.html

//-------------------------------------------------------------

// Delegation.cpp : Defines the entry point for the console application.

//

#include

#ifndef K_DELEGATE_H

#define K_DELEGATE_H

namespace kUTIL

{

// 1. 桥接类(纯虚类):

// 为什么叫作桥接?

// 因为通过它的虚函数方法可以调用到对应的正确的不同派生实例(指后面

// 提到的委托人)所改写的虚函数方法(这是委托人用来完成委托任务的方法)

struct kDelegationInterface

{

virtual ~kDelegationInterface() {};

virtual void Execute() = 0;

};

// 2. 委托类(派生于桥接类,这里我叫为”委托人“)

// 为什么叫委托?

// 因为调用者把“通知”的工作委托给它来负责处理。

// 一个“委托人”保存了: a.”接收者“(对象指针m_pThis) 及 b.“要作的事”(方法指针m_mfn),

// 以便调用者发出信号弹(后面提到,信号弹有一个作桥接用的纯虚类的指针指向相应的委托人)

// 告知此信号对应的委托人来完成它被委托的工作:即让“接收者”(m_pThis)作”要作的事“(m_mfn)。

 

template

struct kDelegationImpl : public kDelegationInterface

{

typedef 
void ( T::* MFN )();

kDelegationImpl( T
* pthis, MFN mfn ) : m_pThis( pthis ), m_mfn( mfn ) {

}


virtual void Execute() {

if( m_pThis ) 

( m_pThis
->*m_mfn )(); 

}


}


T
* m_pThis;

MFN m_mfn;

}
;

 

// 3. 信号弹(实现为仿函数来调用统一的虚函数接口):

// 为什么叫信号?

// 因为当"信号弹"发射时(调用信号的操作符"()")

// 它会通知所指向的"委托人"事件发生了(调用纯虚类指针的m_DI->Execute()方法)。

// 一个信号保存了一个指向对应”委托人“的桥接类(纯虚类)指针。

 

struct kSignal0

{

kDelegationInterface
* m_DI; // 纯虚类的指针

kSignal0() : m_DI(
0{}

 

// 1. 纯虚类的m_DI指针可以指向不同的派生实例:

 

template

void ConnectSlot(T* recv, void (T::* mfn)()) {

DisConnect();

m_DI 
= new kDelegationImpl( recv, mfn );

int test = 0;

}


void DisConnect() {

if( m_DI ) { delete m_DI; m_DI = NULL; }

}

 

// 2. 用统一的纯虚类指针调用不同派生类改写的虚函数

 

void operator() () 

if( m_DI ) {

m_DI
->Execute(); 

}


}


};

 

// 下面是两个为方便使用的函数:

 

template

void kConnect( kSignal0& sgn, T* pObj, void(T::*fn)())

{

sgn.ConnectSlot( pObj, fn );

int i = 0;

}


inline 
void kDisConnect( kSignal0& sgn )

{

sgn.DisConnect();

}


// end of namespace kUTIL

#endif //#ifndef K_DELEGATE_H

 

 

//----------------------------------------------------------------------------

// 一个使用实例:

class kButton {

public:

kUTIL::kSignal0 sgnMouseBtnUp;

void OnMouseButtonUp() { sgnMouseBtnUp(); }

}
;

class kDialog {

kButton btn;

public:

kDialog() 
{

kUTIL::kConnect( btn.sgnMouseBtnUp, 
this&kDialog::DoWork ); // vc6下这里kDialog::DoWork的前面一定要可加"&"号

}


void DoWork() 

__asm 
int 3

}


void TestMouseHit() { btn.OnMouseButtonUp(); }

}
;

int main(int argc, char* argv[])

{

kDialog dlg;

kButton btn;

kUTIL::kConnect( btn.sgnMouseBtnUp, 
&dlg, kDialog::DoWork ); // vc6下这里kDialog::DoWork的前面可加/不加"&"号

// 测试一:



// 测试二:

dlg.TestMouseHit();

return 0;

}

 

 

// 委托实例总结:

// 下面我们来具体说明”当某事发生时,调用者发射信号弹通知对应的接收者作相应处理“

// 1. "调用者" 拥有各种信号弹。

// 2. 初始时,我们把信号弹与对应的委托人联系起来,并让委托人记录在信号触发时应该通知的"接收人"和"接收人该作的事"。

// a. 信号弹保存了桥(纯虚类)指针,指针指向通过其模板函数ConnectSlot方法来找出(产生的)委托人(委托实例)。

// b. 委托人(委托实例)在信号弹用ConnectSlot方法产生它的时候保存了函数ConnectSlot所传入的两个参数:

// 即"接收者指针"及"其方法指针"。

// 3. 当事件发生时"调用者"发射对应信号弹后,信号弹会调用其所保存的纯虚类指针的虚函数方法,

// 于是由于虚函数特性就会调用到其所指向的委托实例(委托人)所改写的方法。

// 5. 委托人改写的方法中通过其所保存的”接收者指针“及其"方法指针"来呼叫"接收者"用对应的”方法指针“

// 来处理事情。

// 即如下流程:

// "调用者"发射"信号弹" ---> "信号弹"通过"桥"找到对应"委托人" ---> "委托人"呼叫"接收者"作"该作的事"

//=============================================================================

嗯,这样到此,一个非常方便的委托类就得以实现了!如果你还不懂的话请仔细的琢磨,如此精华(因为简单而强大)

不要错过。不过上述只是部份实现,当你要支持带参数及返回值的各种情况的话,还得自己作扩充。

返回值的处理方法可参见前面剖述boost的mem_fn的处理方法,而带不同参数的处理则只能一一手动

实现,就象前面所说的那样,这是很无奈的事情,但是目前来说没有办法。。。

(待续。。。。。。,如果有时间有必要的话。。。)

附:

loki下载:

http://sourceforge.net/projects/loki-lib/

boost下载:

http://sourceforge.net/projects/boost/

2004.10.26更新:

修正:

原void connect( Signal0& sgn,T1 obj, void(T2::*fn)())改成

void connect( Signal0& sgn,T1& obj, void(T2::*fn)())

另外加了kDisConnect释放内存,原来只作测试没写它,现在还是加上了。

posted on 2007-09-03 15:22 杨粼波 阅读(413) 评论(0)  编辑 收藏 引用


只有注册用户登录后才能发表评论。
【推荐】超50万行VC++源码: 大型组态工控、电力仿真CAD与GIS源码库
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理