function和bind真的是C++的救赎

  最近整理了以前收藏的那些优秀文章,又发现了孟岩老师的那篇《function/bind的救赎(上)》。曾记得当年读这篇文章的时候硬是没明白啥意思,不过可能随着一些开发经验的积累,经历到的事情也多了,现在也越来越觉得这篇文章真是点中了C++的要害之处。想当初在学校的时候老师们、多少本教材满怀信心地告诉我们:携C++之重器、怀面向对象之思想,仿佛大千世界尽皆可以被这个语言所描述和创造,但是等到真正上船后才发现,不仅C++语言像架波音747一样,非超凡的智慧和丰富的经验不能驾驭,而且蹊跷晦涩的语言特性、一些怪异的语法规则更是暗坑密布,所以业界对这个语言阴暗面的诟病也从未停止。
  不过读过C++之父《C++语言的设计与演化》的人就能理解,C++如此的怪异也是跟其历史来由密切相关的。C++当初为了从C语言那边策反一批用户,坚持高度兼容C语言的抉择不但把C语言的毛病全给沾染了,而且背负着这个沉重的历史包袱也导致C++的发展如同戴着脚镣在艰难的前行着,C++的标准规范的许多例外也都像是一个个的补丁一样那么的扎眼(比如当C++分析语法得到歧义的时候,规定优先将其看做一个声明语句),整体看上去是那么的不和谐。
  其实C++在当初设计的时候如果不是努力维护着和C语言的高度兼容性,以Bjarne的才华,肯定可以把C++设计的远比现在更简洁优雅。不过说实话,让C++既可以像C语言一样可以极为底层的方式高效操作资源,也可以实现面向对象方式的高级抽象,中间还得兼容C语言面向过程的编程手法,其复杂度可想而知,也就必然会造成很多的瑕疵和不完美的地方。而且C++为不完美的C也提供了很多改良性的措施,比如:namespace解决名字冲突,引用代替指针解决指针的种种陷阱,以xxxx_cast的转换语法保证转换操作既安全又方便查找和调试,语言级原生支持多态而不用像在C中使用指针强制转换来模拟多态特性,RAII避免资源潜在泄漏的危险,模板和STL让我们不用一次次的纠结数据结构和算法……上面的这些知识点也告诉我们:我们固然可以用写C的思路去写C++,但是要想写出高效、安全、可维护的代码,那么还是建议认真系统的学习一下C++,善用C++的特性去解决问题。说据实话,越来越感觉Bjarne爷爷真是神人一般的厉害,他在C++的设计演化中恪守着许多哲学和原则,并且始终坚持C++的发展以实用为准则,才使得C++在没有花哨地营销情况下,尽管不够完美也逐渐被大众所接受,而且在工业界占据着极为重要的地位。记得他老人家还开玩笑的表示,如果C++不这么的难以驾驭,老板哪肯给你们多付点薪水呢?
  话题扯远了。孟岩老师只写了个上篇就撒手不管了的节奏,广大C++爱好者在评论下面苦苦央求了快十年,也没有等到后续的更新,但是从其最初的标题也可以猜测出来了:对于C++静态消息分发不灵活的缺陷,需要使用bind和function机制来解决。下面允我先将这篇文章的主题描述下来。
  在面向对象的开发中程序是由很多对象组成的,对象持有自己的资源,他们之间可以灵活组合以便协作完成各项任务,而对象之间是通过以发消息的方式来进行通信的,而发消息的机制大体可以分为两大阵营:静态消息机制和动态消息机制。
  静态消息机制:如果向一个对象发消息,是调用这个对象的成员函数实现的,而要实现成员函数的调用,则调用者必须要知道这个对象的类型信息才可以完成。
  动态消息机制:一般是指与目标对象无关的消息发送机制,发送者不需要知道接收消息的类型是什么、对象是谁、是否能够正确地处理这个消息,而接收消息的对象负责尝试解析这个消息,如果解析成功则最终调用自己的过程来处理该消息,消息通信的过程不再是通过成员函数的调用实现,而是使用对象无关的方式进行的,比如进程间通信、网络等。
  静态消息机制的代表是Simula,而Bjarne承认在创造C++之初的时候对Simula做过深入研究,所以C++也借用了这种静态消息机制。但是这种静态消息机制在使用过程中需要知道通知者的类型信息,这种耦合强度使用起来会显得很受限制,在对设计模式稍有了解的同学都会知道,C++工程设计中会通过大量使用虚函数的方式实现消息通知,通过派生接口类的方式可以把消息发送给这些类,但是虚函数的使用也是有代价的:虚函数机制有虚函数表的额外空间开销和调用开销,设想如果一个类想要作为稍微通用点的消息处理者,比如MFC中常见的消息处理Loop,那么该类就需要通过多继承来实现,随之带来的虚函数机制的开销也是线性增加的;其次是我们需要创建很多的辅助类和辅助函数,那么整个代码就会显得不够优雅、维护成本也更高。
  然后在文章的结尾,孟岩老师提出的救赎之道就是:C++如果要解决静态消息不够灵活的短板,就必须实现对象级别的delegate机制,在C++中如果对象A完成某个事件后需要通知所有感兴趣的观察者(比如对象B),那A不应当依赖于B的名字和类型信息,甚至不需要知道B的存在,只要求B有一个签名正确的方法,就可以通过delegate机制调用B的那个感兴趣的方法就可以了。
  其实看到这里,大家都应该有种恍然大悟的感觉,我觉得孟老不写下篇的原因是答案已经揭晓了(所以下篇也没啥可以写的了)。现在我们对C++语言,已经提出了一个可调用对象的概念,可调用对象包括:函数、函数指针、lambda表达式、bind创建的对象、重载了operator()的类对象,而只要这些对象的签名是一致的,就都可以转换成同一个function来持有,甚至同一个function的对象可以被丢到一个标准容器类中去管理。通过这种方式,我们就无须限定甚至知道消息接收者的类型,只需要他们有约定的可调用对象签名就可以了。
  我们可以举个例子,比如在程序中我们有动态加载运行配置的需求,当配置文件更新后向服务发送一个通知(比如信号、socket报文、甚至是服务自身周期性主动轮训探测),那么所有支持配置动态更新的模块都应该接收到这个通知。很显然,这是一个典型的Observer模式的问题。
  对于传统经典的设计模式,我们可能的实现是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class CfgObserverIf {
public:
virtual int update(int a) = 0;
};

class CfgObserver: public CfgObserverIf {
virtual int update(int a) {
std::cout << "got update:" << a << std::endl;
return 0;
}
};

class CfgMng {
public:
void registerObserver(CfgObserverIf* ob) {
obs_.emplace_back(ob);
}
void notify(int a) {
for(auto iter = obs_.cbegin(); iter != obs_.cend(); ++iter)
(*iter)->update(a);
}
private:
std::vector<CfgObserverIf *> obs_;
};

int main() {
CfgMng mng{};
CfgObserver b{};
mng.registerObserver(&b);
mng.notify(8);
return 0;
}

  然后,我们通过使用function bind,就可以实现一种类似动态消息机制的能力。我们的实现可能就是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
int func(int a) {
std::cout << __func__ << " got update: " << a << std::endl;
return 0;
}

class ModuleA {
public:
int operator()(int a) {
std::cout << __func__ << "got update: " << a << std::endl;
return 0;
}
};

class ModuleB {
public:
int just_kid(int a, int b) {
std::cout << __func__ << " got update: " << a << ", and " << b << std::endl;
return 0;
}
};

typedef std::function<int(int)> CfgObserverCall;
class CfgMng {
public:
void registerObserver(CfgObserverCall ob) {
obs_.emplace_back(ob);
}
void notify(int a) {
for(auto iter = obs_.cbegin(); iter != obs_.cend(); ++iter)
(*iter)(a);
}
private:
std::vector<CfgObserverCall> obs_;
};

int main() {
CfgMng mng{};

mng.registerObserver(func);
mng.registerObserver(ModuleA{});
ModuleB mb{};
mng.registerObserver(std::bind(&ModuleB::just_kid, &mb, 100, std::placeholders::_1));
mng.notify(7);
return 0;
}

  所以,通过fucntion这个函数指针模板持有对象,然后辅助使用bind对函数的接口进行适配,理论上我们可以让任何感兴趣的观察者都将自己注册进来,比如上面例子中的函数、函数对象、普通成员函数,然后在组织者发起通知的时候,任何可调用对象都能得到执行,相比起来是不是前者要显得呆板许多啊。
  得知现在有个Boost.Signal专门做一些信号槽的功能,用以实现通用消息机制,因为我对这个工具还不太了解,此处也就不予讨论了。
  其实,处于一个C++重度使用的公司,我在给周围同事做培训分享的时候也尝试告诉大家:当前Modern C++已经很好用了,大家平时多熟悉一些新特性、新组建,让后批判性的接收这些新技巧就很容易写出稳定、高效的C++程序。在众多的C++新特性中,我首推smart pointer和bind function,前者解决了C++资源泄漏的老大难问题,后者让函数调用更灵活,方便在大型工程中写出更直观、更具维护性的代码。借此还是希望从事C++开发的工作者能多多了解Boost和C++新特性的,如果发现当前使用的C++有很多不完善的地方,或许可以从Boost和C++新标准中找到救赎之道。

本文完!

参考