C++中的delegate

《以boost::function和boost:bind取代虚函数》,原文链接:http://blog.csdn.net/solstice/article/details/3066268 ,很多年前的一篇blog,感觉收获颇丰。

 

第一个问题,虚函数是否可以被取代?--完全可以。可以直接拿作者的例子来举例:

// 一个基于 closure 的 Thread class 基本结构
class Thread 
{ 
 public: 
  typedef boost::function<void()> ThreadCallback; 
  Thread(ThreadCallback cb) : cb_(cb) 
  { } 
  void start() 
  { 
    /* some magic to call run() in new created thread */ 
  } 
 private: 
  void run() 
  { 
    cb_(); 
  } 
  ThreadCallback cb_; 
  // ... 
}; 

使用:
class Foo
{
 public:
  void runInThread();
};</span><span style="font-size:12px;">
</span><span style="font-size:12px;">Foo foo;
Thread thread(boost::bind(&Foo::runInThread, &foo));
thread.start();

//如果按照面向对象的写法,Foo要public一个 IRunable的接口,然后Thread的构造函数为Thread(IRunable *p),使用时候可能是如下:
Foo foo;
Thread thread(foo);
thread.start();</span>

 

      从耦合性上看,显然是虚函数方法的耦合性高,继承和多态不仅规定了函数的名称、参数、返回类型,还规定了类的继承关系。一是Foo类的实现上,Foo如果使用虚函数必须继承接口(继承是非常强的耦合),而使用bind的方法,只要求Foo对象中一个函数的参数与返回值符合要求即可。二是对Thread类的实现上,如果使用虚函数,那么要求参数为一个接口对象IRunable,依赖于一个抽象,如果使用bind方法,那么Thread参数为一个函数对象(姑且把boost::function当作函数对象)类型。如果只考虑IRunable有一个接口的情况,那么IRunable的作用只是一个标识类型的作用,不如函数对象只依赖参数与返回值更“抽象”,也就是说比函数对象类型耦合更紧。如果IRunable有很多的接口,(但是这种情况看实际需求,只有很明确时候才会这样设计,一般要求“接口细化”)这种情况使用的时候函数对象使用起来就很比较繁琐,要设置多次(一个函数对象仅能够表示一个接口)。

     从使用方便性上看,上面过程为对象装配的过程,显然bind的方式代码简洁多了。

     从可读性上看,接口细化,一个IRunable只有一个接口(多数情况),那么其实感觉差不多。

     调试程序上看,boost库首先任何人读起来都很费劲有大量的模板与宏定义,但是如果已经标准化了,为什么要去了解是如何实现的呢。

     从性能上看,不好说,也没测试过,简单理解,bind方法肯定是创建的对象多,但是没有用多态那一套,虚函数方法如果只传递指针,那么从对象创建开销上肯定比bind好,但是有了多态虚指针的开销,而且使用bind + 智能指针的话,对象的销毁可能会延迟很多。

 
第二个问题,虚函数有必要被取代么?--看具体的情况

     向线程库的例子,如果使用bind可以很方便的实现线程的启动,那么这个时候完全可以用bind这一套。 

      其他:虚函数存在是为了多态,也就是为了扩展,为了需求变化,但不是万能的,因为不知道究竟变化到了什么程度。正如作者说的。

 

     “如果是指OO中的public继承,即为了接口与实现分离,那么我只会在派生类的数目和功能完全确定的情况下使用。换句话说,不为将来的扩展考虑,这时候面向对象或许是一种不错的描述方法。一旦要考虑扩展,什么办法都没用,还不如把程序写简单点,将来好大改或重写。”

     “如果是功能继承,那么我会考虑继承boost::noncopyable或boost::enable_shared_from_this,下一篇blog会讲到enable_shared_from_this在实现多线程安全的Signal/Slot时的妙用。”

 

      功能继承可以用聚合的方式来替代,也是耦合太强的问题。

       delegate与虚接口一样,提供了一种“抽象”,依赖抽象编程使得程序具有可扩展性,delegate的抽象是对一个方法的抽象,只限制了参数与返回类型,对名字,继承自什么东西都没有任何要求。可以说是对“继承多态”解决问题的一种补充,也是一种强大的补充。

      很多把delegate翻译成“委托”,这个与设计模式中的委托容易混淆,一开始我觉得这样翻译非常不恰当,但是后来才明白,dalegate的思想就是“委托”,设计模式中的委托(代理),把一个对象注入到另外一个对象中,把一些实际的操作“委托”给另外一个对象执行,而这里的delegate只不过是更加的细化,把一个方法(函数指针或者引用)“委托”给一个对象,然后这个对象可以作为参数注入到某个对象之中,而这个对象像是设计模式中常说的那个干活的对象。 

      A delegate is a form of type-safe function pointer used by the Common Language Infrastructure (CLI). Delegates specify a method to call and optionally an object to call the method on. Delegates are used, among other things, to implement callbacks and event listeners. A delegate object encapsulates a reference to a method. The delegate object can then be passed to code which can call the referenced method, without having to know at compile time which method will be invoked.

 

      1) 参考wiki,最后一句“without having to know at compile time which method will be invoked.”说明了绑定虚函数也是ok能得到正确的运行结果的。
      2) “A delegate is a form of type-safe function pointer”,说delegate为一种类型安全的指针,我理解,这里类型安全是需要语言或者说编译器支持保证,不允许静态类型不匹配的赋值,不能随便把一个指针类型赋值给这个指针,这个指针赋值后不会访问其他“未授权”的内存,狭隘理解,说这个指针访问的只有自己的那一块区域(成员),也就是说只会访问它的referenced method和method的所属对象(如果有的话)。在c++看来,就是一个只能访问自己私有成员的函数对象。

      下面这篇文章谈到的几个观点:function/bind的救赎(上)http://blog.csdn.net/myan/article/details/5928531 ,一下内容多数引用自此文。

 

      “而过程范式和对象范式可以视为对程序本质的两种根本不同的看法,而且能够分别在现实世界中找到相应的映射。

•过程范式认为,程序是由一个又一个过程经过顺序、选择和循环的结构组合而成。反映在现实世界,过程范式体现了劳动分工之前“全能人”的工作特点——所有的事情都能干,所有的资源都是我的,只不过得具体的事情得一步步地来做。
•对象范式则反映了劳动分工之后的团队协作的工作特点——每个人各有所长,各司其职,有各自的私有资源,工件和信息在人们之间彼此传递,最后完成工作。因此,对象范式也就形成了自己对程序的看法——程序是由一组对象组成,这些对象各有所能,通过消息传递实现协作。

重复一遍对象范式的两个基本观念:

•程序是由对象组成的;
•对象之间互相发送消息,协作完成任务;

   请注意,这两个观念与后来我们熟知的面向对象三要素“封装、继承、多态”根本不在一个层面上,倒是与再后来的“组件、接口”神合。 ”

 

      面向对象的三要素更加强调的是如何实现上面的两个基本观念,而不是对上面观念的解释,像一种方法论。

 

      “实际上C++的静态消息机制还引起了更深严重的问题——扭曲了人们对面向对象的理解。既然必须要先知道对象的类型,才能向对象发消息,那么“类”这个概念就特别重要了,而对象只不过是类这个模子里造出来的东西,反而不重要。渐渐的,“面向对象编程”变成了“面向类编程”,“面向类编程”变成了“构造类继承树”。放在眼前的鲜活的对象活动不重要了,反而是其背后的静态类型系统成为关键。“封装、继承”这些第二等的特性,喧宾夺主,俨然成了面向对象的要素。每个程序员似乎都要先成为领域专家,然后成为领域分类学专家,然后构造一个完整的继承树,然后才能new出对象,让程序跑起来。正是因为这个过程太漫长,太困难,再加上C++本身的复杂度就很大,所以C++出现这么多年,真正堪称经典的面向对象类库和框架,几乎屈指可数。很多流行的库,比如MFC、iostream,都暴露出不少问题。一般程序员总觉得是自己的水平不够,于是下更大功夫去练剑。殊不知根本上是方向错了,脱离了对象范式的本质,企图用静态分类法来对现实世界建模,去刻画变化万千的动态世界。这么难的事,你水平再高也很难做好。

   可以从一个具体的例子来理解这个道理,比如在一个GUI系统里,一个 Push Button 的设计问题。事实上在一个实际的程序里,一个 push button 到底“是不是”一个 button,进而是不是一个 window/widget,并不重要,本质上我根本不关心它是什么,它从属于哪一个类,在继承树里处于什么位置,只要那里有这么一个东西,我可以点它,点完了可以发生相应的效果,就可以了。可是Simula –> C++ 所鼓励的面向对象设计风格,非要上来就想清楚,a Push Button is-a Button, a Button is-a Command-Target Control, a Command-Target Control is-a Control, a Control is-a Window. 把这一圈都想透彻之后,才能 new 一个 Push Button,然后才能让它工作。这就形而上学了,这就脱离实际了。所以很难做好。你看到 MFC 的类继承树,觉得设计者太牛了,能把这些层次概念都想清楚,自己的水平还不够,还得修炼。实际上呢,这个设计是经过数不清的失败和钱磨出来、砸出来的。”

 

      c++过于强调,对象“是什么”。

 

     “客观地说,“面向类的设计”并不是没有意义。来源于实践又高于实践的抽象和概念,往往能更有力地把握住现实世界的本质,比如MVC架构,就是这样的有力的抽象。但是这种抽象,应该是来源于长期最佳实践的总结和提高,而不是面对问题时主要的解决思路。过于强调这种抽象,无异于假定程序员各个都是哲学家,具有对现实世界准确而深刻的抽象能力,当然是不符合实际情况的。结果呢,刚学习面向对象没几天的程序员,对眼前鲜活的对象世界视而不见,一个个都煞有介事地去搞哲学冥想,企图越过现实世界,去抽象出其背后本质,当然败得很惨。

      其实C++问世之后不久,这个问题就暴露出来了。第一个C++编译器 Cfront 1.0 是单继承,而到了 Cfront 2.0,加入了多继承。为什么?就是因为使用中人们发现逻辑上似乎完美的静态单继承关系,碰到复杂灵活的现实世界,就破绽百出——蝙蝠是鸟也是兽,水上飞机能飞也能游,它们该如何归类呢?本来这应该促使大家反思继承这个机制本身,但是那个时候全世界陷入继承狂热,于是就开始给继承打补丁,加入多继承,进而加入虚继承,。到了虚继承,明眼人一看便知,这只是一个语法补丁,是为了逃避职责而制造的一块无用的遮羞布,它已经完全已经脱离实践了——有谁在事前能够判断是否应该对基类进行虚继承呢?”

 

      虚继承原来是个补丁,也是,如果是事先知道有钻石型的继承关系,那么开始时候肯定不会那样设计?距离实际的世界差的太远。

 

     “你可能要问,Java 和.NET也是用继承关系组织类库,并进行设计的啊,怎么那么成功呢?这里有三点应该注意。第一,C++的难不仅仅在于其静态结构体系,还有很多源于语言设计上的包袱,比如对C的兼容,比如没有垃圾收集机制,比如对效率的强调,等等。一旦把这些包袱丢掉,设计的难度确实可以大大下降。第二,Java和.NET的核心类库是在C++十几年成功和失败的经验教训基础之上,结合COM体系优点设计实现的,自然要好上一大块。事实上,在Java和.NET核心类库的设计中很多地方,体现的是基于接口的设计,和真正的基于对象的设计。有了这两个主角站台,“面向类的设计”不能喧宾夺主,也能发挥一些好的作用。第三,如后文指出,Java和.NET中分别对C++最大的问题——缺少对象级别的delegate机制做出了自己的回应,这就大大弥补了原来的问题。

    尽管如此,Java还是沾染上了“面向类设计”的癌症,基础类库里就有很多架床叠屋的设计,而J2EE/Java EE当中,这种形而上学的设计也很普遍,所以也引发了好几次轻量化的运动。这方面我并不是太懂,可能需要真正的Java高手出来现身说法。我对Java的看法以前就讲过——平台和语言核心非常好,但风气不好,崇尚华丽繁复的设计,装牛逼的人太多。”

在设计的时候过于强调这个对象“是什么”,按照依赖于抽象的设计,那么这个类必须继承并且实现一些必须的接口(这一定要求非常要命),也就是说问题的重点是一个类必须实现什么“接口”,而在作者认为“消息”与“接口”正是关键所在,作者把c++中通过类对象方法调用的方式称为“静态消息机制”,而正是这种机制导致了“面向类的设计”。

     “COM。COM的要义是,软件是由COM Components组成,components之间彼此通过接口相互通讯。这是否让你回想起本文开篇所提出的对象范型的两个基本原则?有趣的是,在COM的术语里,“COM Component ” 与“object ”通假,这就使COM的心思昭然若揭了。Don Box在Essential COM里开篇就说,COM是更好的C++,事实上就是告诉大家,形而上学的“面向类设计”不好使,还是回到对象吧。

    用COM开发的时候,一个组件“是什么”不重要,它具有什么接口,也就是说,能够对它发什么消息,才是重要的。你可以用IUnknown::QueryInterface问组件能对哪一组消息作出反应。向组件分派消息也不一定要被绑定在方法调用上,如果实现了 IDispatch,还可以实现“自动化”调用,也就是COM术语里的 Automation,而通过 列集(mashal),可以跨进程、跨网络向另一组件发送消息,通过 moniker,可以在分布式系统里定位和发现组件。如果你抱着“对象——消息”的观念去看COM的设计,就会意识到,整个COM体系就是用规范如何做对象,如何发消息的。或者更直白一点,COM就是用C/C++硬是模拟出一个Smalltalk。而且COM的概念世界里没有继承,就其纯洁性而言,比Smalltalk还Smalltalk。在对象泛型上,COM达到了一个高峰,领先于那个时代,甚至于比它的继任.NET还要纯洁。

    COM的主要问题是它的学习难度和安全问题,而且,它过于追求纯洁性,完全放弃了“面向类设计” 的机制,显得有点过。”
   “由于C++的静态消息机制,导致了形而上学的“面向类的设计”,祸害无穷。但实际上,C++是有一个补救机会的,那就是实现对象级别的delegate机制。学过.NET的人,一听delegate这个词就知道是什么意思,但Java里没有对应机制。在C++的术语体系里,所谓对象级别delegate,就是一个对象回调机制。通过delegate,一个对象A可以把一个特定工作,比如处理用户的鼠标事件,委托给另一个对象B的一个方法来完成。A不必知道B的名字,也不用知道它的类型,甚至都不需要知道B的存在,只要求B对象具有一个签名正确的方法,就可以通过delegate把工作交给B的这个方法来执行。在C语言里,这个机制是通过函数指针实现的,所以很自然的,在C++里,我们希望通过指向成员函数的指针来解决类似问题。”

 

      文章最后才提到c++的delegate,delegate的意义不仅仅是代码上写着方便点,或者说c++为了功能的全而增加的一种新的机制,其真正意义在于,如作者对“对象范式的基本概念第二条:对象之间互相发送消息,协作完成任务”的补充,提供了一种新的“对象间消息发送机制”,而这种机制会对设计上产生非常大的影响。
 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++的委托(delegate)是一种函数指针的高级形式,它可以将函数作为参数传递给其他函数或存储在数据结构。委托提供了一种灵活的方式来实现回调机制和事件处理。 在C++,可以使用函数指针、函数对象和Lambda表达式来实现委托。下面是几种常见的委托用法: 1. 函数指针委托: 可以使用函数指针作为委托类型,将一个函数指针赋值给委托变量,然后通过委托变量调用相应的函数。 示例代码: ```cpp void Function1() { // 函数1的实现 } void Function2() { // 函数2的实现 } typedef void (*DelegateType)(); // 定义委托类型 int main() { DelegateType delegate = nullptr; delegate = &Function1; // 将函数1赋值给委托变量 delegate(); // 调用委托,实际上调用了函数1 delegate = &Function2; // 将函数2赋值给委托变量 delegate(); // 调用委托,实际上调用了函数2 return 0; } ``` 2. 函数对象委托: 可以使用函数对象(重载了函数调用运算符operator()的类对象)作为委托类型,将一个函数对象赋值给委托变量,然后通过委托变量调用相应的函数。 示例代码: ```cpp class FunctionObject { public: void operator()() { // 函数对象的实现 } }; int main() { FunctionObject functionObject; typedef void (FunctionObject::*DelegateType)(); // 定义委托类型 DelegateType delegate = nullptr; delegate = &FunctionObject::operator(); // 将函数对象的函数调用运算符赋值给委托变量 (functionObject.*delegate)(); // 调用委托,实际上调用了函数对象的函数调用运算符 return 0; } ``` 3. Lambda表达式委托: 可以使用Lambda表达式作为委托类型,直接将Lambda表达式赋值给委托变量,然后通过委托变量调用相应的函数。 示例代码: ```cpp int main() { auto lambda = []() { // Lambda表达式的实现 }; typedef decltype(lambda) DelegateType; // 定义委托类型 DelegateType delegate = lambda; // 将Lambda表达式赋值给委托变量 delegate(); // 调用委托,实际上调用了Lambda表达式 return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值