C++中的delegate

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

// 一个基于 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();

从耦合性上看,显然是虚函数方法的耦合性高,继承和多态不仅规定了函数的名称、参数、返回类型,还规定了类的继承关系。一是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.

下面这篇文章谈到的几个观点:function/bind的救赎(上) ,以下内容多数引用自此文。

而过程范式和对象范式可以视为对程序本质的两种根本不同的看法,而且能够分别在现实世界中找到相应的映射。 过程范式认为,程序是由一个又一个过程经过顺序、选择和循环的结构组合而成。反映在现实世界,过程范式体现了劳动分工之前“全能人”的工作特点——所有的事情都能干,所有的资源都是我的,只不过得具体的事情得一步步地来做。 对象范式则反映了劳动分工之后的团队协作的工作特点——每个人各有所长,各司其职,有各自的私有资源,工件和信息在人们之间彼此传递,最后完成工作。因此,对象范式也就形成了自己对程序的看法——程序是由一组对象组成,这些对象各有所能,通过消息传递实现协作。 重复一遍对象范式的两个基本观念: 程序是由对象组成的; 对象之间互相发送消息,协作完成任务; 请注意,这两个观念与后来我们熟知的面向对象三要素“封装、继承、多态”根本不在一个层面上,倒是与再后来的“组件、接口”神合。 ”

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

实际上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++为了功能的全而增加的一种新的机制,其真正意义在于,如作者对“对象范式的基本概念第二条:对象之间互相发送消息,协作完成任务”的补充,提供了一种新的“对象间消息发送机制”,而这种机制会对设计上产生非常大的影响。

参考

以boost::function和boost:bind取代虚函数

Table of Contents