Singleton#2 关于全局对象,初始化依赖,死引用,线程安全

此前曾经总结过一篇关于Singleton实现方式的文章,在这里:
http://www.windameister.org/blog/index.php/2009/01/18/singleton-initialization/
 
本文是对上文中未能涵盖的一些问题的补充。
 
首先是关于全局对象的,全局对象的某些缺陷正是促使我们使用Singleton的原因之一。看下面这个例子:
 
// xxx.cpp
GlobalData g_Global1;
 
GlobalData2 g_Global2;
 
在一个编译单元(某个CPP)中定义的全局变量,会按照定义的先后次序进行构造。如上定义的情况下,程序初始化阶段,会先构造g_Global1对象,而后构造g_Global2。然而我们曾经提到过,如果GlobalData的构造函数依赖于g_Global2则会产生问题,因为此时g_Global2还未被构造。
 
另外,在析构的时候,会按照与构造相反的顺序析构,也即先析构g_Global2再析构g_Global1。然而如果GlobalData的析构函数依赖于g_Global2,也会产生问题,因为到g_Global1析构的时候,此时的g_Global2已经析构完毕了,很可能所访问的地址已经是无效的。
 
当然我们可以按照对象相互之间初始化的依赖关系来确定其定义的顺序,然而这种感觉似乎不那么靠谱。首先,这种方法只在单个编译单元范围内起作用,而对跨编译单元的全局对象则无解(因为link的时候,会将各个cpp编译出来的obj文件链接到一起,而这个顺序并没有任何标准进行规定,各家编译器厂商都有自己的实现方式),如果cppA中有一个全局对象globalA,cppB中有一个globalB,我们不能推断出这两者的初始化顺序孰先孰后。
 
Imperfect C++的11.1.3中提到了一种控制全局对象初始化顺序的方案:
GlobalData* g_Global1;
GlobalData2* g_Global2;
int main()
{
         GlobalData _global1;
         GlobalData2 _global2;
         g_Global1 = &_global1;
         g_Global2 = &_global2;
         …
}
 
通过将全局对象定义于main函数的栈空间中,我们可以获得对初始化顺序的完全控制权。然而这样的做法也有其特有的缺陷和局限性:
1、  全局指针可能被某个客户代码中的全局对象所拥有,并在析构中针对这个指针做些什么,如果是这样的话,由于全局对象析构时,main函数已经退出,并且其中的对象已经析构完毕,我们又一次遇到了针对已死亡对象的操作。
2、  我们可能并不总能拥有自己编写main函数的权利,当我们在利用某个现有框架进行编码的时候,很可能我们自己并不能拥有对入口函数的控制权,那么这种方法就很难奏效了。
 
以上是对全局对象的简单总结。事实上,全局对象和Singleton同样面临多线程竞争条件的问题,这也是全局对象不被推荐使用的原因之一。
 
下面再回到我们的正题Singleton:
 
Scott Meyers曾经描述过一种单件的实现途径,后被人们称为Meyers单件。大致的实现看起来如下:
 
class Thing
{
public:
         Thing& GetInstance()
         {
                   static Thing s_instance;
                   return s_instance;
}
private:
         Thing(Thing const&);
         Thing &operator = (Thing const&);
}
 
通过GetInstance方法,使用lazy-evaluation的方式来按需创建。(该方法在云风的《游戏之旅——我的编程感悟》一书中也有提及)。上述实现存在几个问题:
1、  局部静态对象——在多线程情况下将会导致竞争条件
2、  基于局部静态对象的前提条件,并且GetInstance方法被实现为内联函数,如果该头文件被两个模块共享,在某些编译器上会导致在不同模块中分别创建单件对象实例的情况。(感兴趣的话,可以用VC6做个实验)
3、  你无法控制该对象的生命期,该对象在第一次被使用时创建,与其他静态对象一起随进程关闭机制来销毁。(准确的说是在Main函数结束之后,会执行到一个专门做析构的代码块,这部分代码由编译器生成,作为程序的编写者,我们无从控制其析构顺序)可能另一个静态对象的析构函数,在s_instance析构之后再调用Thing::GetInstance(),这等于是在与一个已死去的人交谈,着被称为死引用(Dead Reference)。
 
针对死引用(Dead Reference)问题,Alexandrescu在Loki库中展示了一种Singleton技术,通过将单件对象挂钩到语言清理机制上,以确保他们按照一个相对寿命级别被销毁。这种方式,要求程序员对单件的寿命级别进行手工赋值,然而这很难保证不出错。
 
Imperfect C++(中文版166页)中提出了另一种针对死引用问题的解决方案,简单的说就是引用计数。假设A是一个单件,模块B,C都要依赖于A,A提供两个函数A_init()和A_Uninit(),这两个函数对A被依赖的次数进行计数,只有到被依赖值为0的时候,才释放自身。B和C在初始化以及销毁时分别调用A_init()和A_Uninit()。这样就可以保证任何一个依赖于A的模块都不会在A已经死亡之后再调用A。在这里,该方案确实能解决死引用问题,只要调用者记得在依赖A的模块中调用A_init()/A_Uninit()即可保证这一点。
 
关于多线程竞争条件:
此前已经多次提到单件对象存在多线程竞争条件的问题。比如Meyers单件中,情况看起来如下:
         Thing& GetInstance()
         {
                   static Thing s_instance;
                   return s_instance;
}
 
多线程条件下究竟会发生什么呢?
我们将上述代码改写一下,更容易看清楚发生了什么:经过编译器处理之后的上述代码,看起来类似于下面的样子:
Thing& GetInstance()
{
         static bool [...]

Win32平台下基于SVN开发的若干问题整理

 

Keywords: svn  svn_cmdline_init crash 1.5.6 win32

 SVN(SubVersion)是目前开源的版本管理工具中较为流行的,最新的release是1.6.1。但是遗憾的是基于SVN开发的资料在互联网上异常的少。

         最近一段时间,我在工作上需要编写一个使用SVN进行版本管理的编辑器。(需要对编辑器生成的文件做版本管理以利于多人合作开发)起初是通过调用svn自身携带的的客户端程序svn.exe以实现相关功能,然而由于实际中的若干设计问题,最终的速度很不理想。于是产生了借用svn自身提供的api接口实现所需功能的想法。
 
         在实际使用svn提供的api时遇到了一些问题,经过多番查找资料以及尝试最终得以解决。将问题整理在此,以方便日后与我遇到类似问题的朋友。
 
         Svn提供的第三方开发接口sdk可以在这里下载到。目前最新的release是1.6.1。我在项目中实际使用的版本是1.5.6。开发需要的几个压缩包分别如下:
Svn发布的2进制可运行文件,包括客户端与服务端,以及所需的dll:
http://subversion.tigris.org/files/documents/15/45230/svn-win32-1.5.6.zip
Svn开发所需的lib及头文件以及文档:
http://subversion.tigris.org/files/documents/15/45236/svn-win32-1.5.6_dev.zip
Svn的调试符号文件:
http://subversion.tigris.org/files/documents/15/45234/svn-win32-1.5.6_pdb.zip
 
此外,在编写基于SVN的程序时,在链接时还有一些必要的库需要添加,分别是:
Berkeley DB for Windows:
http://subversion.tigris.org/files/documents/15/32472/db-4.4.20-win32.zip
libintl binaries for Windows:
http://subversion.tigris.org/files/documents/15/20739/svn-win32-libintl.zip
 
有了这几个包,就可以使用SVN提供的api开发基于SVN的第三方软件了。
 
         基于SVN1.5及之后的版本开发的时候,会遇到一个CRT冲突的问题,该问题会导致在调用svn_cmdline_init时,如果在error stream参数中传入非NULL值,则会直接crash。一个针对该问题的描述可以参见这里。
         我下载了调试符号以及源代码之后,跟踪调试到svn_cmdline_init函数当中,发现crash发生在一个CRT函数(setvbuf)的调用当中。实际跟踪调试的过程中发现,虽然该函数是crt中的函数,然而vc调试器却无法基于本地的调试符号正确跟踪到其源文件当中。
又该问题在1.4及之前的版本不存在。究其原因,我猜测可能是由于自1.5版本之后,SVN提供的库是基于动态链接的CRT库的。而svn本身的lib在编译时链接的crt与客户的VC6的CRT版本不符导致的。(1.4版本之前则是静态链接的CRT)
对该问题的描述还可以参见这里:
 
The problem is stderr — it is a FILE* which is a CRT type which means 
you have to be using the same CRT as the dll’s were built with. 
Unfortunately, there are a few API’s where raw CRT types [...]

Ogre源码剖析3–可扩展性&插件机制

Ogre是一个跨操作系统平台的开源3D引擎,既支持DirectX,也支持使用OpenGL,支持可替换的场景管理算法(BSP, OCT)。为Ogre提供这些灵活可扩展性的基础之一就是其面向插件的设计。
 
很多常用的软件大都提供了插件接口,用以扩展应用程序设计者最初未想到的功能,比较常见的譬如PhotoShop的滤镜,After Effect中的各种插件(最有名的比如shine),3dMax的插件譬如渲染器,魔兽世界的辅助插件等等。
通常,插件本身通常也需要实现主应用程序所需要的必要接口,从而使得插件可以被应用程序加载执行。此外,插件的实现也需要由主应用程序提供一些接口api,通过这些接口,插件可以对主应用程序的功能进行调用。
插件可以是动态链接库(win32平台上为DLL文件),也可以是以脚本的形式提供的,比如魔兽世界中的插件就是使用lua编写的,插件也可能是某种应用程序自定义的文件,只要该应用程序提供了创建该类文件的方法并实现解析、执行功能即可。(不同的实现形式各有利弊,具体需要参考插件及应用程序所处的运行环境进行取舍)
采用插件的一个巨大的好处,以及很多应用程序中使用插件的主要目的就是,我们可以在不需要改动应用程序本身的情况下扩展应用程序的功能。
 
在Ogre中,插件被用来提供渲染子系统(RenderSystem),不同类别的图形API被封装在不同的渲染子系统的视线当中,Ogre默认提供了DX和OpenGL的实现,甚至,如果我们乐意,甚至可以只用绘点函数实现一套纯软件的渲染子系统提供给Ogre使用。以此为例,用简单的形式来展示这种实现大概类似于下面这样:
class RenderSystem
{
         // … operations that a render system need to support
};
// In DX_RenderSystem.dll (in plugin dx rendersystem )
class DX_RenderSystem : public RenderSystem
{
         // … implementation & override of the operations from RenderSystem using DirectX
};
// In GL_RenderSystem.dll (in plugin openGL rendersystem)
class GL_RenderSystem : public RenderSystem
{
         // … implementation & override of the operations [...]

小议Command模式 抽象在实际编程中的应用

在许多编辑器中,大都实现了Undo / Redo操作。从界面出发,Undo/Redo仅仅只是一种抽象的”提供单一接口行为“的操作,尽管实际的Undo/Redo工作可能会涉及各种不同的具体操作。举个例子,在3dMax这样的软件中,我们可以创建3D实体,可以编辑其自身的各种属性(譬如立方体有长宽高,球有半径),可以移动实体的位置,对其做旋转,缩放操作,乃至对个别的网格顶点,面片的细微调节。其中的每一步,都可以撤销或重做。
我们仔细思考一下,就会明白,撤销和重做实际上是需要保存每一步操作的”被操作实体的两个状态“(分别是该实体在操作发生前和发生后的状态)。
譬如对一个物体做以移动,我们实际上是保存了移动前的位置,和移动后的位置,这样在Undo/Redo时,只需要分别将其置于所需的状态即可。
而不管具体的状态是如何转变的,对于用户来说,撤销/重做都只是一个简单的命令,表现为一个按钮,或者一次Ctrl+Z。这其实就是一种具有同一性表现的动作,在软件实现中,非常适合使用Command模式对其进行抽象。
 
譬如,我们只需要定义这样的一个基类即可明晰撤销/重做操作的概念接口:
class Command {
public:
         virtual ~ Command() = 0 {}
         virtual bool Do() = 0;
         virtual bool Undo() = 0;
bool Redo() { Do(); }
};
 
所有的Undo/Redo的状态,都派生自这个Command抽象类,它定义了两个纯虚函数,用于做和撤销。Redo函数只是简单的调用Do()实现其功能。
 
有了上面这个接口,当我们想添加更多类型操作的Undo/Redo支持的时候,只需要这样做:
class MoveBoxCommand{
public:
         MoveBoxCommand(VECTOR3 vNewPos, int boxid)
                   : m_iID(boxid), m_vNewPos(vNewPos) {}
         ~MoveBoxCommand() {}
bool Do()
{
m_vOldPos = SomeManager->GetObject(m_iID)->GetPos();
         return SomeManager->GetObject(m_iID)->SetPos(m_vNewPos);
}
bool Undo()
{
return SomeManager->GetObject(m_iID)->SetPos(m_vOldPos);
}
private:
int m_iID;
VECTOR3 m_vNewPos;
VECTOR3 m_vOldPos;
};
 
现在我们就可以对任意的对象支持Move操作的撤销/重做了,事实上,旋转,缩放等其他操作大都与此类似,对添加删除之类的操作需要做一点特别的处理(因为我们不能简单的把实际对象释放掉,否则其他引用了该对象的Command就会出现问题,一种良好的做法是设置删除标志)。
除此之外,我们需要将所有的操作产生的Command实例放在一个管理器中统一管理,假设其被命名为CommandManager。则CommandManager需要负责维护当前的Command,需要在用户调用Undo时,判断是否能够Undo,如果能够,则取出下一个Undo的Command,调用其Undo,并将当前Command的指针或者索引更新位置。
如果用户在某个点做出了新的操作,则需要将当前Command之后的所有Command清空。(如果用户没有做新操作,则可以使用当前Command之后的Command来实现Redo操作的,但是新操作会冲掉位于Redo序列中的元素)
如果撤销/重做的实现有了以上理解,就不难理解为什么几乎所有的软件在用户Undo多步,再采取新动作之后,就无法Redo了。
 
通过这种方式,我们就可以实现一个具有可扩展性的撤销重做机制了,不论再加入何种类型的新操作,我们都可以设法将其纳入到上述的框架当中。
 
具体的撤销重做实现还牵扯到许多细节,不过那都与主题关系不大,我们再来看一个例子:线程池。
 
有时我们可能会想编写一个线程池,用于处理各种并发的任务。线程池从概念上说是一个很简单的东西,有一个线程的集合,里面的很多线程都睡着,当有任务来的时候,一个线程跳出来把任务领走并执行,执行结束后,线程继续回到池子里睡觉。如果有多个任务,那么就会有许多个线程都跳出来,直到池子里没有更多的空闲线程或者任务队列被领空为止。
 
在Win32平台下编写多线程程序,大致会像这样:
void ThreadProc(PVOID param)
{
         // do something
}
 
// somewhere in some func, [...]

学习是什么?——从未知到已知

当我们掌握了一项技能时,我们说我们学会了某样技能。当我们掌握了某种知识时,我们说我们学到了某样事实。
 
如果从上述的角度来定义学习,应该说学习是我们将某种未知变为已知的一个过程。
 
将某种东西从未知变为已知,往往会对我们产生莫大的影响。譬如,每多掌握一门语言就为我们开启了了解更多广博的信息的又一扇大门。再譬如,每多掌握一种理论工具,就为我们认识这个世界增加了一种视角(例如对于一件社会事件,我们可以通过各种不同的方法去认识,社会心理学的,经济学的,行为决策学的)。再譬如,每多掌握一种技能,我们就增加了自身的能力,例如学会开车让我们可以驾驶方便快捷的代步工具,学会烹饪让我们可以以较低的成本获取较为卫生美味的食物。
 
从未知到已知对我们的改变是巨大的,然而,在这一过程中,我们自身究竟发生了什么变化?
学习这一事实,对我们自身究竟造成了什么影响?
 
从还原论的角度出发,我们可以简单的得出结论–学习改变了我们的大脑。再具体一些,学习是我们对未知事物产生记忆的过程(其中技能性质的学习,是产生了序列记忆,譬如学会走路,跑步,骑自行车,开车,用筷子吃饭等等;显式的知识性质的学习,则是产生了某种容易在需要的时候被提取的外显记忆;而环境中的种种刺激则对形成了内隐记忆,我们无法显示的提取出相应的记忆,但是会对特定的信息呈现反应)。
学习改变了我们大脑诸多复杂的神经网络中的某些神经回路的连接(连接是否存在以及对刺激信号的反应),从而我们得以将未知的事情变为已知的事情–在事件再次发生时,激活我们业已形成的神经回路,并引发我们的反应。
 
但是,同样是花费时间学习,乃至是学习同样的知识,为什么有的人的学习成果就比另一些人更为显著?即便在我们排除了智力、年龄、以及记忆障碍等客观差异的情况下,为什么不同的人的学习效果的差别依旧非常巨大?
 
学习的典型步骤往往是这样的:知道存在某个事实,了解事实的实际知识,知道该事实如何发生,理解事实的背后成因, 以及将该事实与自己已知的事实相关联。
 
善于学习的人,懂得如何举一反三,引用笑来老师的话说,这个道理还可以运用在什么地方?对同样知识的学习的效果差异就出现在这个地方,善于学习的人,会将新学到的知识,尽可能多的与已有的知识相关联,这样在日后才有可能在遇到这些有关问题的时候将学习到的知识提取出来,并运用之于解决问题。
而不善于关联的人,则往往发现学了很多东西,却用不上。
为什么用不上?因为在遇到特定问题的时候(问题是对我们的神经系统的某种刺激信号),我们的大脑中缺乏对应的神经回路,以使得这一刺激信号激活我们大脑中存储了特定知识的区域,在外在表现上则是我们没办法找到与该问题关联的信息,用以帮助我们处理问题和形成决策。
 
学习不仅仅包括主动的在外显记忆中形成新的关联。对内隐记忆进行抽取和明了也是极大的学习。内隐记忆源于我们大脑下意识存储的过往经验,但是这种经验却没有进入到我们的意识当中。
往往有这样的情况,在我们在对一个问题形成看法时,会受自己的好恶的影响,这些好恶我们自己也说不清楚来由,就是强烈的觉得这样好,或者这样不好。但是自己也说不明白这种强烈好恶是否有足够的理由支撑。
实际上,我们并非没有说清理由的能力,而是大脑的机制让我们快速的跳过了必要的推理过程,而到达了某种结论(这种机制在一些特定的状况下是有好处的,尤其是在紧急情况下,但是在较为重大的决策的时刻,却存在很大隐患)。如果我们能够将问题逐步理性的理清,并逐条的思考,就会发现在当前的状况下,自己的”条件反射式”的好恶,并不一定绝对正确,我们的大脑在做出这种”条件反射式”的结论的过程中可能忽略了某些必要条件,又或者引入了某些假设而不自知。
 
有效的学习需要我们将未知信息的刺激进行编码,形成某种已知的知识;并且需要我们将该知识与我们已有的知识进行关联,形成特定的神经回路,以便更加容易的在需要的时候进行提取;还需要我们对下意识经验产生的条件反射去做有意识的分析,将隐含于内隐记忆中的知识,提取理清,并关联到我们业已形成的外显记忆的神经网络当中。

面子是什么?从何而来?我们如何应对?

Wiki的解释里:脸面指的是人在社会中的道德信力/社会威望。
(http://en.wikipedia.org/wiki/Face_(social_concept))
 
“长脸”(有面子):通常意指维护一个好的形象。http://en.wikipedia.org/wiki/Face_saving
“丢脸”(丢面子):通常指在他人面前形象被降低。
 
通常情况下,在中国社会中,为了避免冲突,人们往往会顾及他人的面子而不会在公众场合提起一些让人难为情的事情。
 
如果A说了一些话,导致B当众出丑/难堪(抑或是隐性的失去足够具有公信力的形象)。往往会导致B的反抗,引发B恼羞成怒的情形,诸如此类。
 
但是,如果注意观察的话,我们会发现一个事实:
对于他人损伤了我们的“面子”这个事实,我们往往并非因为分析理清了这件事对我们造成了什么损失而产生愤怒的情绪反应,而是在各种有伤我们“面子”的事件发生时,大脑以及身体自行产生了愤怒的情绪。
 
如果我们维护“面子”的行为,是身体以及大脑自行产生的本能反应,那么也就是说,这种行为是进化所赋予我们的一种心理机制。这种心理机制以及其引发的行为,会给我们的祖先带来某种生存优势以使得带有这种心理机制的人更容易将其基因传递下来。
 
为什么维护“面子”可以带来生存优势?
 
假设1:群居生存的祖先们,某人(称之为A)更擅长捕猎,往往会获得更多的食物,他在这个群体中就更容易获得威信,往往会成为群落的头领。从而,他能够在食物分配,配偶选择,居住条件等问题上获得更好的选择权。(在食物贫乏的时候,他优先获得食物,遍可以获得更多的生存优势,而他擅长捕猎的特点,可以为这个群体获得更多食物,因此保障此人的精力充沛,可以使得群体的生存几率更大;他可以选择更健康的配偶,以使得后代有更大的概率获得较好的体质)
 
我们设想一下,假若此人的“面子”/威信被质疑,会导致什么情况发生:
 
如果群落中有一个人B认为A的能力不够好,那么对A来说,他的特权就遭到了挑战。为什么?因为如果B证明了A并不是最好的头领,那么对于这个群落来说,A的价值就有疑问了,他不配再享有优先的食物供给,配偶选择,住所选择权了。那么,如果A直接妥协了,他就失去了他一直以来享有的这一切,这显然是A难以忍受的,A必须捍卫自己的威信以保障自己的利益和特权。这可能会直接引发A与B之间的激烈冲突,A在此时必须证明自己的能力,如果他不能维护自己的威信,即便这一次B在冲突中失利,群落中的人对A的能力和公信力的评价也会降低,这同样会导致A的特权遭到损伤。
 
我们在群居的动物生活中也能观察到类似上述例子的情形。
 
假设结论1:有“面子”本身并不会为我们带来任何实质上的好处,有“面子”实际上是我们所能对群体创造的价值所带来附加的属性。
 
正常情况下,如果我们能够给社会创造极大的价值,那么就自然会获得公信力,社会威望,我们在社会资源分配中就能够享有优先的权利。这种情形,在他人眼中,就是“有面子”的体现。
 
一个现实问题:
现实社会往往较为复杂,尽管一个可持续发展的正常社会的总体趋势必然是创造的越多,在分配中所获得的也越多,否则如果创造价值的人总是得不到应有的收获,那么他必然会放弃为社会创造价值的努力,从而造成社会创造价值的总值降低,并使得社会群体中的每一个人的生活幸福程度降低。
 
事实上,扭曲创造价值与收获分配的社会是存在的。
当生产力发展到可以解决温饱问题的时候,社会就存在这种扭曲的可能性。只要这种扭曲不会导致社会整体的崩溃。
一个很现实的例子就是中国古代对读书人的尊崇。读书人不从事生产劳动,无需上缴税赋,然而却在人为的扭曲的情况下,获得了较高的社会地位(相比农,工,商)。
读书人,相比农民,工人,商贾,更加的“有面子”,但是对社会创造的价值,却无法匹配其所拥有的社会地位。
 
类似的西方的例子还有古罗马被日耳曼人灭亡之前的社会状况。
 
片面的追求“面子”,并不能给社会总体带来任何好处。即便通过扭曲的社会价值观,可能给自身带来一些好处,但是造成的结果必然是社会整体效率的降低。
 
不仅如此,片面的追求“面子”还可能带来很多负面影响,具体的可以参考goust在云风老大的《再谈平等》一文下的评论。
http://blog.codingnow.com/2009/02/ouie.html#comment-36634
 
 

Singleton#1——关于单件对象初始化的探讨

凡是使用C++进行开发的人,大都或是了解,或是直接使用过Singleton模式,但是Singleton的多种实现方式有什么差异?不同的实现细节背后究竟蕴含着什么意义?本文试图列举常见的几种不同的Singleton实现方式,考察这些不同的实现方式中的细节差异,并剖析其好处与缺点,试图对Singleton的实现方式做一个较为完整的总结。

我们在使用C++编写实际项目的时候,往往会有对全局对象/变量的访问需求。对全局变量的不加封装的访问,会导致许多麻烦的问题:譬如在调试中,由于对全局变量的修改被分散在程序的各个角落里,使得我们无法准确判断一个错误是在何时发生的;再比如,在多线程环境下,如果多个线程都需要访问一个变量,则需要做线程同步操作,在没有良好的封装的情况下,这种状况可能会导致完全莫名的coredump。不过本文并不是想说Singleton有什么好处或者有什么作用,仅仅只是想总结Singleton的不同实现方式之间的异同,以及适用场合。

Ogre源码剖析2:如何实现低耦合的类间消息传递机制

关于低耦合的消息传递,实现的方式有很多,哪种方法更好与具体的使用环境有关,本文使用试错的方法,逐步探索达成这一目的具体方式,并理解实现方式背后的原因。
 
面向对象的系统当中,不可避免的有大量的类间消息传递的需求:一个类需要通知另一个或几个类做些什么。
 
这种类间消息传递,简单的说,就是调用其他类的方法。
如下:
void A::OnMessageXX()
{
         B::GetInstance()->DoSomething();
}
在这里,类A需要通知类B做些事情。这种调用在所有的面向对象程序中都是极其常见的。
 
但是如果类A需要调用类B,就不可避免的产生了耦合性。虽然耦合性终归是不可能完全避免的,但是在一定程度上降低耦合性是完全可能的。
(至于为什么在设计中应该尽可能降低耦合性,不在本文的探讨范围之内)
 
上面的例子,我们使用了Singleton的模式,从全局作用域中获取了B的实例,并调用了B的相关方法。使用Singleton的一个缺点是,假若我们希望对类A编写测试代码,我们需要做一些额外的解耦合工作。(关于编写测试与解耦合,可以参考Robert C. Martin Series的Working Effectively with Legacy Code一书,该书的中译版在这)
我们也可以通过将B参数化的方法降低A与B间的耦合程度,像下面这样:
void A::OnMessageXX(B* pBInstance)
{
         pBInstance->DoSomething();
}
现在的写法要比之前的做法耦合性低,通过使用多态的方法,现在传入函数的类B指针可能是另一个实现了B的相应接口的派生类,A并不关心B接口背后的具体实现。
 
但是等等,你说,现在对类B的耦合性虽然在A中被降低了,但是依旧存在于调用A::OnMessageXX的地方。在那里我们还是需要取得B的实例,然后传递给A。
 
没错,是这样。
 
通过参数化类A的方法,我们把类A与类B间的耦合转移了一部分到A的调用者那里。实际上总的耦合并没有消除,只是被分解了。但是程序设计中不可能完全不存在耦合,我们需要做的是”正确”,而不是”完美”。类A的耦合性降低了,使得我们在未来需求变更的时候,类A有更大的可能性不需要被修改,并且对功能的扩展更加友好,这就达成了我们的目标了。
基于上述做法,如果我们在未来扩展是派生出一个B的子类,override相关的方法,那么类A的代码基本是不需要修改的。
 
不过,问题是,假若A::OnMessageXX中,并不仅仅需要对类B发出消息,还需要对一系列相关的类B1,B2,B3等等发出消息呢?
 
哦,或许我们可以这样做:
 
void A::OnMessageXX(const std::list<B*>& lstBInstances)
{
         for (std::list<B*>::const_iterator itr = lstBInstances.begin();
                   itr != lstBInstances.end();
                   ++itr)
         {
                   (*itr)->DoSomething();
}
}
 
是的,上面这是一种做法,有一系列B的对象需要被通知到,所以我们可以用一个列表把他们串起来,然后在循环中通知他们去干活。不过这样做的前提是,这一系列B对象都是派生自一个公共基类B,有共通的接口;此外,我们需要在A的OnMessageXX被调用之前构造一个需要接受通知的B对象列表。
 
当A需要通知B,C,D等一系列没有公共接口的对象的时候,上面的这种做法就无法处理了。
 
对于B、C、D等需要由A来调用的类来说,它们需要在A通知它们的时候,做一些特定的事情。而又A则是在某些特定的时刻需要通知B、C、D。这样,我们可以把问题看成一个消息响应机制。
 
B、C、D可以在A的某些事件上注册一些回调函数,当事件发生时,A确保注册该事件的函数被调用到。
 
如下:
typedef void(callback*)();
class A {
public:
         enum EventIds {
         EVENT_MSG1,
         EVENT_MSG2,
};
void RegisterEvent(int nEventId, callback pfn);
private:
callback m_pfnCallback;
};
 
现在,B可以调用A::RegisterEvent注册一个事件,并传递一个函数指针给A。
当A中发生了注册的事件时,这个函数指针会被回调到。
不过这种简单的做法适应性很差:
1、  不能支持单个事件的多个callback (可能有很多类都需要注册该事件,并在事件发生时依次被回调)
2、  不能支持多个事件的同时存在
3、  回调函数没有参数’
 
针对问题1,2,我们可以使用一个事件映射解决问题,做法如下:
typedef int EventId;
typedef void (callback*)();
typedef std::list<callback> CallbackList;
typedef std::map<EventId, CallbackList> CallbackMap;
现在这个数据结构就能够支持多个event同时存在,且每个event都可以支持多个回调函数了。
 
但是这种用法依旧很不方便,如果类B想要注册A上的一个事件,他需要定义一个 callback类型的函数,并把这个函数的地址传递给A。问题是,往往我们希望类B的回调函数在被调用到的时候,对类B中的数据和状态进行修改,而一个单独的函数,若想获得/修改B中的状态,则必须要与类B紧密耦合。(通过获取全局对象,或者Singleton的方式)
这种紧密耦合引发我们的思考,能否在Callback中同时包含类B的指针与类B的成员函数。
 
答案是肯定的:泛型回调就可以做到这一点。关于泛型回调(Generic callback)的信息,在Herb [...]

Ogre源码剖析1:任意类型类Any

有些时候我们可能想做这样一件事:
float f = 1.f;
int n = 2;
std::vector<X> myContainer;           // X是一个虚构的用户定义类型
myContainer.pushback(X(f));
myContainer.pushback(X(n));
 
我们想在一个容器里保存两种乃至多种不同的数据类型。
但是,显然普通的模板参数如std::vector<int>,或者std::vector<float>都无法满足我们的需求。
 
也可能存在下面这样的情况:
float fVal = 1.f;
X xMsg = fVal;
PushMessage(xMsg); // PushMessage发送了一个异步消息
 
// 另一个地方(可能是另一个线程),回调函数被调用:
OnMessageXXX(X xMsg)
{
         float f = static_cast<float>(xMsg);
         // do something with value f
}
 
针对这种需求,我们想要一种类型,它可以接受任意的类型,并在需要的时候把我们放入的真正的类型取出来,它可以被放置到容器中,可以被拷贝,以及串行化(前提要求被置入其中的类型可以被输出到标准输出流–std::ostream当中)。
 
如何设计这种类型呢?
 
一种容易想到的方案如下:
 
class Any {
public:
enum {
                  ANYTYPE_INT,
                  ANYTYPE_FLOAT,
                  ANYTYPE_LONG,
                  // … other data types
};
 
union {
                   int nIntData;
                   float fFloatData;
                   long lLongData;
                   // … other [...]