应该用bind+function bind this取代虚函数吗

C++工程实践(5):避免使用虚函数作为库的接口
我的图书馆
C++工程实践(5):避免使用虚函数作为库的接口
陈硕 (giantchen_AT_gmail)
Blog.csdn.net/Solstice
摘要:作为 C++ 动态库的作者,应当避免使用虚函数作为库的接口。这么做会给保持二进制兼容性带来很大麻烦,不得不增加很多不必要的 interfaces,最终重蹈 COM 的覆辙。
本文主要讨论 Linux x86 平台,会继续举 Windows/COM 作为反面教材。
本文是上一篇《》的延续,在写这篇文章的时候,我原本以外大家都对“以虚函数作为接口”的害处达成共识,我就写得比较简略,看来情况不是这样,我还得展开谈一谈。
“接口”有广义和狭义之分,本文用中文“接口”表示广义的接口,即一个库的代码界面;用英文 interface 表示狭义的接口,即只包含 virtual function 的 class,这种 class 通常没有 data member,在 Java 里有一个专门的关键字 interface 来表示它。
C++ 程序库的作者的生存环境
假设你是一个 shared library 的维护者,你的 library 被公司另外两三个团队使用了。你发现了一个安全漏洞,或者某个会导致 crash 的 bug 需要紧急修复,那么你修复之后,能不能直接部署 library 的二进制文件?有没有破坏二进制兼容性?会不会破坏别人团队已经编译好的投入生成环境的可执行文件?是不是要强迫别的团队重新编译链接,把可执行文件也发布新版本?会不会打乱别人的 release cycle?这些都是工程开发中经常要遇到的问题。
如果你打算新写一个 C++ library,那么通常要做以下几个决策:
以什么方式发布?动态库还是静态库?(本文不考虑源代码发布这种情况,这其实和静态库类似。)
以什么方式暴露库的接口?可选的做法有:以全局(含 namespace 级别)函数为接口、以 class 的 non-virtual 成员函数为接口、以 virtual 函数为接口(interface)。
(Java 程序员没有这么多需要考虑的,直接写 class 成员函数就行,最多考虑一下要不要给 method 或 class 标上 final。也不必考虑动态库静态库,都是 .jar 文件。)
在作出上面两个决策之前,我们考虑两个基本假设:
代码会有 bug,库也不例外。将来可能会发布 bug fixes。
会有新的功能需求。写代码不是一锤子买卖,总是会有新的需求冒出来,需要程序员往库里增加东西。这是好事情,让程序员不丢饭碗。
(如果你的代码第一次发布的时候就已经做到完美,将来不需要任何修改,那么怎么做都行,也就不必继续阅读本文。)
也就是说,在设计库的时候必须要考虑将来如何升级。
基于以上两个基本假设来做决定。第一个决定很好做,如果需要 hot fix,那么只能用动态库;否则,在分布式系统中使用静态库更容易部署,这在前文中已经谈过。(“动态库比静态库节约内存”这种优势在今天看来已不太重要。)
以下本文假定你或者你的老板选择以动态库方式发布,即发布 .so 或 .dll 文件,来看看第二个决定怎么做。(再说一句,如果你能够以静态库方式发布,后面的麻烦都不会遇到。)
第二个决定不是那么容易做,关键问题是,要选择一种可扩展的 (extensible) 接口风格,让库的升级变得更轻松。“升级”有两层意思:
对于 bug fix only 的升级,二进制库文件的替换应该兼容现有的二进制可执行文件,二进制兼容性方面的问题已经在前文谈过,这里从略。
对于新增功能的升级,应该对客户代码的友好。升级库之后,客户端使用新功能的代价应该比较小。只需要包含新的头文件(这一步都可以省略,如果新功能已经加入原有的头文件中),然后编写新代码即可。而且,不要在客户代码中留下垃圾,后文我们会谈到什么是垃圾。
在讨论虚函数接口的弊端之前,我们先看看虚函数做接口的常见用法。
虚函数作为库的接口的两大用途
虚函数为接口大致有这么两种用法:
调用,也就是库提供一个什么功能(比如绘图 Graphics),以虚函数为接口方式暴露给客户端代码。客户端代码一般不需要继承这个 interface,而是直接调用其 member function。这么做据说是有利于接口和实现分离,我认为纯属脱了裤子放屁。
回调,也就是事件通知,比如网络库的“连接建立”、“数据到达”、“连接断开”等等。客户端代码一般会继承这个 interface,然后把对象实例注册到库里边,等库来回调自己。一般来说客户端不会自己去调用这些 member function,除非是为了写单元测试,模拟库的行为。
混合,一个 class 既可以被客户端代码继承用作回调,又可以被客户端直接调用。说实话我没看出这么做的好处,但实际中某些面向对象的 C++ 库就是这么设计的。
对于“回调”方式,现代 C++ 有更好的做法,即 boost::function + boost::bind,见参考文献[4],muduo 的回调全部采用这种新方法,见《》。本文以下不考虑以虚函数为回调的过时的做法。
对于“调用”方式,这里举一个虚构的图形库,这个库的功能是画线、画矩形、画圆弧:
1: struct Point
7: class Graphics
virtual void drawLine(int x0, int y0, int x1, int y1);
virtual void drawLine(Point p0, Point p1);
virtual void drawRectangle(int x0, int y0, int x1, int y1);
virtual void drawRectangle(Point p0, Point p1);
virtual void drawArc(int x, int y, int r);
virtual void drawArc(Point p, int r);
这里略去了很多与本文主题无关细节,比如 Graphics 的构造与析构、draw*() 函数应该是 public、Graphics 应该不允许复制,还比如 Graphics 可能会用 pure virtual functions 等等,这些都不影响本文的讨论。
这个 Graphics 库的使用很简单,客户端看起来是这个样子。
Graphics* g = getGraphics();
g-&drawLine(0, 0, 100, 200);
releaseGraphics(g); g = NULL;
似乎一切都很好,阳光明媚,符合“面向对象的原则”,但是一旦考虑升级,事情立刻复杂起来。
虚函数作为接口的弊端
以虚函数作为接口在二进制兼容性方面有本质困难:“一旦发布,不能修改”。
假如我需要给 Graphics 增加几个绘图函数,同时保持二进制兼容性。这几个新函数的坐标以浮点数表示,我理想中的新接口是:
--- old/graphics.h
13:12:44. +0800
+++ new/graphics.h
13:13:30. +0800
@@ -7,11 +7,14 @@
class Graphics
virtual void drawLine(int x0, int y0, int x1, int y1);
virtual void drawLine(double x0, double y0, double x1, double y1);
virtual void drawLine(Point p0, Point p1);
virtual void drawRectangle(int x0, int y0, int x1, int y1);
virtual void drawRectangle(double x0, double y0, double x1, double y1);
virtual void drawRectangle(Point p0, Point p1);
virtual void drawArc(int x, int y, int r);
virtual void drawArc(double x, double y, double r);
virtual void drawArc(Point p, int r);
受 C++ 二进制兼容性方面的限制,我们不能这么做。其本质问题在于 C++ 以 vtable[offset] 方式实现虚函数调用,而 offset 又是根据虚函数声明的位置隐式确定的,这造成了脆弱性。我增加了 drawLine(double x0, double y0, double x1, double y1),造成 vtable 的排列发生了变化,现有的二进制可执行文件无法再用旧的 offset 调用到正确的函数。
怎么办呢?有一种危险且丑陋的做法:把新的虚函数放到 interface 的末尾,例如:
--- old/graphics.h
13:12:44. +0800
+++ new/graphics.h
13:58:22. +0800
@@ -7,11 +7,15 @@
class Graphics
virtual void drawLine(int x0, int y0, int x1, int y1);
virtual void drawLine(Point p0, Point p1);
virtual void drawRectangle(int x0, int y0, int x1, int y1);
virtual void drawRectangle(Point p0, Point p1);
virtual void drawArc(int x, int y, int r);
virtual void drawArc(Point p, int r);
virtual void drawLine(double x0, double y0, double x1, double y1);
virtual void drawRectangle(double x0, double y0, double x1, double y1);
virtual void drawArc(double x, double y, double r);
这么做很丑陋,因为新的 drawLine(double x0, double y0, double x1, double y1) 函数没有和原来的 drawLine() 函数呆在一起,造成阅读上的不便。这么做同时很危险,因为 Graphics 如果被继承,那么新增虚函数会改变派生类中的 vtable offset 变化,同样不是二进制兼容的。
另外有两种似乎安全的做法,这也是 COM 采用的办法:
1. 通过链式继承来扩展现有 interface,例如
--- graphics.h
13:12:44. +0800
+++ graphics2.h
13:58:35. +0800
@@ -7,11 +7,19 @@
class Graphics
virtual void drawLine(int x0, int y0, int x1, int y1);
virtual void drawLine(Point p0, Point p1);
virtual void drawRectangle(int x0, int y0, int x1, int y1);
virtual void drawRectangle(Point p0, Point p1);
virtual void drawArc(int x, int y, int r);
virtual void drawArc(Point p, int r);
+class Graphics2 : public Graphics
using Graphics::drawL
using Graphics::drawR
using Graphics::drawA
// added in version 2
virtual void drawLine(double x0, double y0, double x1, double y1);
virtual void drawRectangle(double x0, double y0, double x1, double y1);
virtual void drawArc(double x, double y, double r);
将来如果继续增加功能,那么还会有 class Graphics3 : public Graphics2;以及 class Graphics4 : public Graphics3 等等。这么做和前面的做法一样丑陋,因为新的 drawLine(double x0, double y0, double x1, double y1) 函数位于派生 Graphics2 interace 中,没有和原来的 drawLine() 函数呆在一起,造成割裂。
2. 通过多重继承来扩展现有 interface,例如定义一个与 Graphics class 有同样成员的 Graphics2
--- graphics.h
13:12:44. +0800
+++ graphics2.h
13:16:45. +0800
@@ -7,11 +7,32 @@
class Graphics
virtual void drawLine(int x0, int y0, int x1, int y1);
virtual void drawLine(Point p0, Point p1);
virtual void drawRectangle(int x0, int y0, int x1, int y1);
virtual void drawRectangle(Point p0, Point p1);
virtual void drawArc(int x, int y, int r);
virtual void drawArc(Point p, int r);
+class Graphics2
virtual void drawLine(int x0, int y0, int x1, int y1);
virtual void drawLine(double x0, double y0, double x1, double y1);
virtual void drawLine(Point p0, Point p1);
virtual void drawRectangle(int x0, int y0, int x1, int y1);
virtual void drawRectangle(double x0, double y0, double x1, double y1);
virtual void drawRectangle(Point p0, Point p1);
virtual void drawArc(int x, int y, int r);
virtual void drawArc(double x, double y, double r);
virtual void drawArc(Point p, int r);
+// 在实现中采用多重接口继承
+class GraphicsImpl : public Graphics,
// version 1
public Graphics2, // version 2
这种带版本的 interface 的做法在 COM 使用者的眼中看起来是很正常的,解决了二进制兼容性的问题,客户端源代码也不受影响。
在我看来带版本的 interface 实在是很丑陋,因为每次改动都引入了新的 interface class,会造成日后客户端代码难以管理。比如,如果代码使用了 Graphics3 的功能,要不要把现有的 Graphics2 都替换掉?
如果不替换,一个程序同时依赖多个版本的 Graphics,一直背着历史包袱。依赖的 Graphics 版本愈来愈多,将来如何管理得过来?
如果要替换,为什么不相干的代码(现有的运行得好好的使用 Graphics2 的代码)也会因为别处用到了 Graphics3 而被修改?
这种二难境地纯粹是“以虚函数为库的接口”造成的。如果我们能直接原地扩充 class Graphics,就不会有这些屁事,见本文“推荐做法”一节。
假如 Linux 系统调用以 COM 接口方式实现
或许上面这个 Graphics 的例子太简单,没有让“以虚函数为接口”的缺点充分暴露出来,让我们看一个真实的案例:Linux Kernel。
Linux kernel 从 0.10 的 发展到 2.6.37 的 ,kernel interface 一直在扩充,而且保持良好的兼容性,它保持兼容性的办法很土,就是给每个 system call 赋予一个终身不变的数字代号,等于把虚函数表的排列固定下来。点开本段开头的两个链接,你就能看到 fork() 在 Linux 0.10 和 Linux 2.6.37 里的代号都是 2。(系统调用的编号跟硬件平台有关,这里我们看的是 x86 32-bit 平台。)
试想假如 Linus 当初选择用 COM 接口的链式继承风格来描述,将会是怎样一种壮观的景象?为了避免扰乱视线,请移步观看。(先后关系与版本号不一定 100% 准确,我是用 git blame 去查的,现在列出的代码只从 0.01 到 2.5.31,相信已经足以展现 COM 接口方式的弊端。)
不要误认为“接口一旦发布就不能更改”是天经地义的,那不过是“以 C++ 虚函数为接口”的固有弊端,如果跳出这个框框去思考,其实 C++ 库的接口很容易做得更好。
为什么不能改?还不是因为用了C++ 虚函数作为接口。Java 的 interface 可以添加新函数,C 语言的库也可以添加新的全局函数,C++ class 也可以添加新 non-virtual 成员函数和 namespace 级别的 non-member 函数,这些都不需要继承出新 interface 就能扩充原有接口。偏偏 COM 的 interface 不能原地扩充,只能通过继承来 workaround,产生一堆带版本的 interfaces。有人说 COM 是二进制兼容性的正面例子,某深不以为然。COM 确实以一种最丑陋的方式做到了“二进制兼容”。脆弱与僵硬就是以 C++ 虚函数为接口的宿命。
相反,Linux 系统调用以编译期常数方式固定下来,万年不变,轻而易举地解决了这个问题。在其他面向对象语言(Java/C#)中,我也没有见过每改动一次就给 interface 递增版本号的做法。
还是应了《》中的那句话,Explicit is better than implicit, Flat is better than nested.
动态库的接口的推荐做法
取决于动态库的使用范围,有两类做法。
如果,动态库的使用范围比较窄,比如本团队内部的两三个程序在用,用户都是受控的,要发布新版本也比较容易协调,那么不用太费事,只要做好发布的版本管理就行了。再在可执行文件中使用 rpath 把库的完整路径确定下来。
比如现在 Graphics 库发布了 1.1.0 和 1.2.0 两个版本,这两个版本可以不必是二进制兼容。用户的代码从 1.1.0 升级到 1.2.0 的时候要重新编译一下,反正他们要用新功能都是要重新编译代码的。如果要原地打补丁,那么 1.1.1 应该和 1.1.0 二进制兼容,而 1.2.1 应该和 1.2.0 兼容。如果要加入新的功能,而新的功能与 1.2.0 不兼容,那么应该发布到 1.3.0 版本。
为了便于检查二进制兼容性,可考虑把库的代码的暴露情况分辨清楚。muduo 的头文件和 class 就有意识地分为用户可见和用户不可见两部分,见。对于用户可见的部分,升级时要注意二进制兼容性,选用合理的版本号;对于用户不可见的部分,在升级库的时候就不必在意。另外 muduo 本身设计来是以静态库方式发布,在二进制兼容性方面没有做太多的考虑。
如果库的使用范围很广,用户很多,各家的 release cycle 不尽相同,那么推荐 [2, item 43],并考虑多采用 non-member non-friend function in namespace [1, item 23] [2, item 44 abd 57] 作为接口。这里以前面的 Graphics 为例,说明 pimpl 的基本手法。
1. 暴露的接口里边不要有虚函数,而且 sizeof(Graphics) == sizeof(Graphics::Impl*)。
class Graphics
Graphics(); // outline ctor
~Graphics(); // outline dtor
void drawLine(int x0, int y0, int x1, int y1);
void drawLine(Point p0, Point p1);
void drawRectangle(int x0, int y0, int x1, int y1);
void drawRectangle(Point p0, Point p1);
void drawArc(int x, int y, int r);
void drawArc(Point p, int r);
boost::scoped_ptr&Impl&
2. 在库的实现中把调用转发 (forward) 给实现 Graphics::Impl ,这部分代码位于 .so/.dll 中,随库的升级一起变化。
#include &graphics.h&
class Graphics::Impl
void drawLine(int x0, int y0, int x1, int y1);
void drawLine(Point p0, Point p1);
void drawRectangle(int x0, int y0, int x1, int y1);
void drawRectangle(Point p0, Point p1);
void drawArc(int x, int y, int r);
void drawArc(Point p, int r);
Graphics::Graphics()
: impl(new Impl)
Graphics::~Graphics()
void Graphics::drawLine(int x0, int y0, int x1, int y1)
impl-&drawLine(x0, y0, x1, y1);
void Graphics::drawLine(Point p0, Point p1)
impl-&drawLine(p0, p1);
3. 如果要加入新的功能,不必通过继承来扩展,可以原地修改,且保持二进制兼容性。先动头文件:
--- old/graphics.h
15:34:06. +0800
+++ new/graphics.h
15:14:12. +0800
@@ -7,19 +7,22 @@
class Graphics
Graphics(); // outline ctor
~Graphics(); // outline dtor
void drawLine(int x0, int y0, int x1, int y1);
void drawLine(double x0, double y0, double x1, double y1);
void drawLine(Point p0, Point p1);
void drawRectangle(int x0, int y0, int x1, int y1);
void drawRectangle(double x0, double y0, double x1, double y1);
void drawRectangle(Point p0, Point p1);
void drawArc(int x, int y, int r);
void drawArc(double x, double y, double r);
void drawArc(Point p, int r);
boost::scoped_ptr&Impl&
然后在实现文件里增加 forward,这么做不会破坏二进制兼容性,因为增加 non-virtual 函数不影响现有的可执行文件。
--- old/graphics.cc
15:15:20. +0800
+++ new/graphics.cc
15:15:26. +0800
@@ -1,35 +1,43 @@
#include &graphics.h&
class Graphics::Impl
void drawLine(int x0, int y0, int x1, int y1);
void drawLine(double x0, double y0, double x1, double y1);
void drawLine(Point p0, Point p1);
void drawRectangle(int x0, int y0, int x1, int y1);
void drawRectangle(double x0, double y0, double x1, double y1);
void drawRectangle(Point p0, Point p1);
void drawArc(int x, int y, int r);
void drawArc(double x, double y, double r);
void drawArc(Point p, int r);
Graphics::Graphics()
: impl(new Impl)
Graphics::~Graphics()
void Graphics::drawLine(int x0, int y0, int x1, int y1)
impl-&drawLine(x0, y0, x1, y1);
+void Graphics::drawLine(double x0, double y0, double x1, double y1)
impl-&drawLine(x0, y0, x1, y1);
void Graphics::drawLine(Point p0, Point p1)
impl-&drawLine(p0, p1);
采用 pimpl 多了一道 forward 的手续,带来的好处是可扩展性与二进制兼容性,通常是划算的。pimpl 扮演了的作用。
pimpl 不仅 C++ 语言可以用,C 语言的库同样可以用,一样带来二进制兼容性的好处,比如 libevent2 里边的 struct event_base 是个 opaque pointer,客户端看不到其成员,都是通过 libevent 的函数和它打交道,这样库的版本升级比较容易做到二进制兼容。
为什么 non-virtual 函数比 virtual 函数更健壮?因为 virtual function 是 bind-by-vtable-offset,而 non-virtual function 是 bind-by-name。加载器 (loader) 会在程序启动时做决议(resolution),通过 mangled name 把可执行文件和动态库链接到一起。就像使用 Internet 域名比使用 IP 地址更能适应变化一样。
万一要跨语言怎么办?很简单,暴露 C 语言的接口。Java 有 JNI 可以调用 C 语言的代码,Python/Perl/Ruby 等等的解释器都是 C 语言编写的,使用 C 函数也不在话下。C 函数是万能的接口,C 语言是最伟大的系统编程语言。
本文只谈了使用 class 为接口,其实用 free function 有时候更好(比如
muduo/base/Timestamp.h 除了定义 class Timestamp,还定义了 muduo::timeDifference() 等 free function),这也是 C++ 比 Java 等纯面向对象语言优越的地方。留给将来再细谈吧。
[1] Scott Meyers, 《Effective C++》 第 3 版,条款 35:考虑 virtual 函数以外的其他选择;条款 23:宁以 non-member、non-friend 替换 member 函数。
[2] Herb Sutter and Andrei Alexandrescu, 《C++ 编程规范》,条款 39:考虑将 virtual 函数做成 non-public,将 public 函数做成 non-virtual;条款 43:明智地使用 pimpl;条款 44:尽可能编写 nonmember, nonfriend 函数;条款 57:将 class 和其非成员函数接口放入同一个 namespace。
[3] 孟岩,《》,《》中的“四个半抽象”。
[4] 陈硕,《》,《》。
本作品采用进行许可。
TA的推荐TA的最新馆藏[转]&[转]&[转]&[转]&
喜欢该文的人也喜欢&&&&&&&&&&&&&                     作者:天涯 来源:中国自学编程网
这是一篇比较情绪化的blog,中心思想是“继承就像一条贼船,上去就下不来了”,而借助boost::function和boost::bind,大多数情况下,你都不用上贼船。&
boost::function和boost::bind已经纳入了std::tr1,这或许是C++0x最值得期待的功能,它将彻底改变C++库的设计方式,以及应用程序的编写方式。&
Scott Meyers的Effective C++ 3rd ed.第35条款提到了以boost::function和boost:bind取代虚函数的做法,这里谈谈我自己使用的感受。&
boost::function就像C#里的delegate,可以指向任何函数,包括成员函数。当用bind把某个成员函数绑到某个对象上时,我们得到了一个closure(闭包)。例如:&
class Foo&
void methodA();&
void methodInt(int a);&
class Bar&
void methodB();&
boost::function&void()& f1; // 无参数,无返回值&
f1 = boost::bind(&Foo::methodA, &foo);&
f1(); // 调用 foo.methodA();&
f1 = boost::bind(&Bar::methodB, &bar);&
f1(); // 调用 bar.methodB();&
f1 = boost::bind(&Foo::methodInt, &foo, 42);&
f1(); // 调用 foo.methodInt(42);&
boost::function&void(int)& f2; // int 参数,无返回值&
f2 = boost::bind(&Foo::methodInt, &foo, _1);&
f2(53); // 调用 foo.methodInt(53);&
如果没有boost::bind,那么boost::function就什么都不是,而有了bind(),“同一个类的不同对象可以delegate给不同的实现,从而实现不同的行为”(myan语),简直就无敌了。&
对程序库的影响&
程序库的设计不应该给使用者带来不必要的限制(耦合),而继承是仅次于最强的一种耦合(最强耦合的是友元)。如果一个程序库限制其使用者必须从某个class派生,那么我觉得这是一个糟糕的设计。不巧的是,目前有些程序库就是这么做的。&
例1:线程库&
常规OO设计:&
写一个Thread base class,含有(纯)虚函数 Thread#run(),然后应用程序派生一个继承class,覆写run()。程序里的每一种线程对应一个Thread的派生类。例如Java的Thread可以这么用。&
缺点:如果一个class的三个method需要在三个不同的线程中执行,就得写helper class(es)并玩一些OO把戏。&
基于closure的设计:&
令Thread是一个具体类,其构造函数接受Callable对象。应用程序只需提供一个Callable对象,创建一份Thread实体,调用Thread#start()即可。Java的Thread也可以这么用,传入一个Runnable对象。C#的Thread只支持这一种用法,构造函数的参数是delegate ThreadStart。boost::thread也只支持这种用法。&
// 一个基于 closure 的 Thread class 基本结构&
class Thread&
typedef boost::function&void()& ThreadC&
Thread(ThreadCallback cb) : cb_(cb)&
void start()&
/* some magic to call run() in new created thread */&
void run()&
ThreadCallback cb_;&
class Foo&
void runInThread();&
Thread thread(boost::bind(&Foo::runInThread, &foo));&
thread.start();&
例2:网络库&
以boost::function作为桥梁,NetServer class对其使用者没有任何类型上的限制,只对成员函数的参数和返回类型有限制。使用者EchoService也完全不知道NetServer的存在,只要在main()里把两者装配到一起,程序就跑起来了。&
// library&
class NetServer : boost::noncopyable&
typedef boost::function&void (Connection*)& ConnectionC&
typedef boost::function&void (Connection*, const void*, int len)& MessageC&
NetServer(uint16_t port);&
~NetServer();&
void registerConnectionCallback(const ConnectionCallback&);&
void registerMessageCallback(const MessageCallback&);&
void sendMessage(Connection*, const void* buf, int len);&
class EchoService&
typedef boost::function&void(Connection*, const void*, int)& SendMessageC // 符合NetServer::sendMessage的原型&
EchoService(const SendMessageCallback& sendMsgCb)&
: sendMessageCb_(sendMsgCb)&
void onMessage(Connection* conn, const void* buf, int size) // 符合NetServer::NetServer::MessageCallback的原型&
printf(&Received Msg from Connection %d: %.*s\n&, conn-&id(), size, (const char*)buf);&
sendMessageCb_(conn, buf, size); // echo back&
void onConnection(Connection* conn) // 符合NetServer::NetServer::ConnectionCallback的原型&
printf(&Connection from %s:%d is %s\n&, conn-&ipAddr(), conn-&port(), conn-&connected() ? &UP& : &DOWN&);&
SendMessageCallback sendMessageCb_;&
// 扮演上帝的角色,把各部件拼起来&
int main()&
NetServer server(7);&
EchoService echo(bind(&NetServer::sendMessage, &server, _1, _2, _3));&
server.registerMessageCallback(bind(&EchoService::onMessage, &echo, _1, _2, _3));&
server.registerConnectionCallback(bind(&EchoService::onConnection, &echo, _1));
server.run();&
对面向对象程序设计的影响&
一直以来,我对面向对象有一种厌恶感,叠床架屋,绕来绕去的,一拳拳打在棉花上,不解决实际问题。面向对象三要素是封装、继承和多态。我认为封装是根本的,继承和多态则是可有可无。用class来表示concept,这是根本的;至于继承和多态,其耦合性太强,往往不划算。&
继承和多态不仅规定了函数的名称、参数、返回类型,还规定了类的继承关系。在现代的OO编程语言里,借助反射和attribute/annotation,已经大大放宽了限制。举例来说,JUnit 3.x 是用反射,找出派生类里的名字符合 void test*() 的函数来执行,这里就没继承什么事,只是对函数的名称有部分限制(继承是全面限制,一字不差)。至于JUnit 4.x 和 NUnit 2.x 则更进一步,以annoatation/attribute来标明test case,更没继承什么事了。&
我的猜测是,当初提出面向对象的时候,closure还没有一个通用的实现,所以它没能算作基本的抽象工具之一。现在既然closure已经这么方便了,或许我们应该重新审视面向对象设计,至少不要那么滥用继承。&
自从找到了boost::function+boost::bind这对神兵利器,不用再考虑类直接的继承关系,只需要基于对象的设计(object-based),拳拳到肉,程序写起来顿时顺手了很多。&
对面向对象设计模式的影响&
既然虚函数能用closure代替,那么很多OO设计模式,尤其是行为模式,失去了存在的必要。另外,既然没有继承体系,那么创建型模式似乎也没啥用了。&
最明显的是Strategy,不用累赘的Strategy基类和ConcreteStrategyA、ConcreteStrategyB等派生类,一个boost::function&&成员就解决问题。在《设计模式》这本书提到了23个模式,我认为iterator有用(或许再加个State),其他都在摆谱,拉虚架子,没啥用。或许它们解决了面向对象中的常见问题,不过要是我的程序里连面向对象(指继承和多态)都不用,那似乎也不用叨扰面向对象设计模式了。&
或许closure-based programming将作为一种新的programming paradiam而流行起来。&
依赖注入与单元测试&
前面的EchoService可算是依赖注入的例子,EchoService需要一个什么东西来发送消息,它对这个“东西”的要求只是函数原型满足SendMessageCallback,而并不关系数据到底发到网络上还是发到控制台。在正常使用的时候,数据应该发给网络,而在做单元测试的时候,数据应该发给某个DataSink。&
安照面向对象的思路,先写一个AbstractDataSink interface,包含sendMessage()这个虚函数,然后派生出两个classes:NetDataSink和MockDataSink,前面那个干活用,后面那个单元测试用。EchoService的构造函数应该以AbstractDataSink*为参数,这样就实现了所谓的接口与实现分离。&
我认为这么做纯粹是脱了裤子放屁,直接传入一个SendMessageCallback对象就能解决问题。在单元测试的时候,可以boost::bind()到MockServer上,或某个全局函数上,完全不用继承和虚函数,也不会影响现有的设计。&
什么时候使用继承?&
如果是指OO中的public继承,即为了接口与实现分离,那么我只会在派生类的数目和功能完全确定的情况下使用。换句话说,不为将来的扩展考虑,这时候面向对象或许是一种不错的描述方法。一旦要考虑扩展,什么办法都没用,还不如把程序写简单点,将来好大改或重写。&
如果是功能继承,那么我会考虑继承boost::noncopyable或boost::enable_shared_from_this,下一篇blog会讲到enable_shared_from_this在实现多线程安全的Signal/Slot时的妙用。&
例如,IO-Multiplex在不同的操作系统下有不同的推荐实现,最通用的select(),POSIX的poll(),Linux的epoll(),FreeBSD的kqueue等等,数目固定,功能也完全确定,不用考虑扩展。那么设计一个NetLoop base class加若干具体classes就是不错的解决办法。&
基于接口的设计&
这个问题来自那个经典的讨论:不会飞的企鹅(Penguin)究竟应不应该继承自鸟(Bird),如果Bird定义了virtual function fly()的话。讨论的结果是,把具体的行为提出来,作为interface,比如Flyable(能飞的),Runnable(能跑的),然后让企鹅实现Runnable,麻雀实现Flyable和Runnable。(其实麻雀只能双脚跳,不能跑,这里不作深究。)&
进一步的讨论表明,interface的粒度应足够小,或许包含一个method就够了,那么interface实际上退化成了给类型打的标签(tag)。在这种情况下,完全可以使用boost::function来代替,比如:&
// 企鹅能游泳,也能跑&
class Penguin&
void run();&
void swim();&
// 麻雀能飞,也能跑&
class Sparrow&
void fly();&
void run();&
// 以 closure 作为接口&
typedef boost::function&void()& FlyC&
typedef boost::function&void()& RunC&
typedef boost::function&void()& SwimC&
// 一个既用到run,也用到fly的客户class&
class Foo&
Foo(FlyCallback flyCb, RunCallback runCb) : flyCb_(flyCb), runCb_(runCb)&
FlyCallback flyCb_;&
RunCallback runCb_;&
// 一个既用到run,也用到swim的客户class&
class Bar&
Bar(SwimCallback swimCb, RunCallback runCb) : swimCb_(swimCb), runCb_(runCb)&
SwimCallback swimCb_;&
RunCallback runCb_;&
int main()&
// 装配起来,Foo要麻雀,Bar要企鹅。&
Foo foo(bind(&Sparrow::fly, &s), bind(&Sparrow::run, &s));&
Bar bar(bind(&Penguin::swim, &p), bind(&Penguin::run, &p));
实现Signal/Slot&
boost::function + boost::bind 描述了一对一的回调,在项目中,我们借助boost::shared_ptr + boost::weak_ptr简洁地实现了多播(multi-cast),即一对多的回调,并且考虑了对象的生命期管理与多线程安全;并且,自然地,对使用者的类型不作任何限制,篇幅略长,留作下一篇blog吧。(boost::signals也实现了Signal/Slot,但可惜不是线程安全的。)
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:1233938次
积分:12670
积分:12670
排名:第1091名
原创:164篇
转载:263篇
评论:106条
(1)(1)(2)(2)(12)(3)(1)(7)(12)(13)(9)(24)(22)(5)(12)(11)(2)(5)(4)(9)(2)(2)(2)(10)(7)(15)(2)(9)(26)(9)(4)(18)(8)(19)(12)(9)(19)(13)(16)(30)(15)(19)(4)

我要回帖

更多关于 function bind this 的文章

 

随机推荐