Ogre很多地方采用了单件模式,单件模式的好处在于维护某个类在程序中只有唯一的实例,实例可以在程序的命名空间中的如何地方被直接调用,这样就避免了对实例指针的传递。
LogManager ,ControllerManager, DyLibManager等都是使用的单例模式。
既然知道了单例模式的好处和Ogre的广泛用途,少年你肯定想知道Ogre的singleton是如果实现的吧。下面我们结合LogManager来对它研究一番。
单例模式大家都知道(PS : 不知道的少年可以参考《设计模式》一书),我们下面主要针对普通Singleton和Ogre的Singleton的相同点和不同点来进行一下分析
相同点:
1 维护某个类在程序中只有唯一的实例,都可以通过getSingleton() , getSingletonPtr()或instance()等方式来获得唯一的实例。
2 因为要用到不同的派生类中,一般把Singleton提出来用template加以实现,这样派生类直接继承Singleton,并实例化模板参数即可,如下:
template <typename T> class Singleton class _OgreExport LogManager : public Singleton<LogManager>, public LogAlloc
其中的LogAlloc,这里暂时不展开来谈,等到分析Ogre内存分配的时候,再详细的说明。对于继承于Singleton的类的具体实现,我们要特化Singleton的static成员变量,对于LogManager:
template<> LogManager* Singleton<LogManager>::msSingleton = 0;
不同点:
1. 普通的Singleton如(boost Singleton)构造函数为protected,程序调用instance的地方生成实例,在需要的地方直接调用instance或者getSingleton这类的函数即可:
// singleton.hpp template <class T> /*static*/ T &singleton<T>::instance() { // function-local static to force this to work correctly at static // initialization time. static singleton<T> s_oT; return(s_oT); } MyClass::instance().doSomething();
而Ogre的Singleton有个特点在于它的构造函数是public的,初始化的而且是需要new的,如下:
mLogManager = OGRE_NEW LogManager(); mLogManager->createLog(logFileName, true, true);
但在之后调用的时候就直接getSingleton就可以了。
我认为Ogre这样做的原因,是把最耗时间的初始化操作放在程序的开始,而等到真实渲染的时候,不必再为初始化instance而耗费时间,这个设计的缺点在于初始化的时间耗费比较多。
2. Ogre使用单例比较特殊的地方,在于在每个单例里面,他都会重写,而重写的函数只调用基类Singleton的方法,如下:
static LogManager& getSingleton(void); static LogManager* getSingletonPtr(void);
这样做的好处在于把模板的操作限定在单个类的实现中,而编译器不需要编程时再跑到Singleton的头文件中,再对模板进行编译。当你如果dll中导出的单件时候,不是基于单个文件的模板类就会出问题,会出现链接错误的情况,经本人确认的确如此,如果在具体的单件中重写两个方法就会把static的实例化限制在单个编译单元中,而函数只需要调用基类的方法,这样可以避免link error。
3. 因为Ogre跨平台和支持多编译器的特性,在Singleton 的构造函数都会对编译器的版本做一次判断,如下:
Singleton( void ) { assert( !msSingleton ); #if defined( _MSC_VER ) && _MSC_VER < 1200 int offset = (int)(T*)1 - (int)(Singleton <T>*)(T*)1; msSingleton = (T*)((int)this + offset); #else msSingleton = static_cast< T* >( this ); #endif }
比较难理解的地方在于#if defined( _MSC_VER ) && _MSC_VER < 1200 这行的预编译判断,<1200 意味着 vc的版本小于 vc6.0 (囧 那么老的编译器还有人用吗) ,static的内存模型就会不同,需要做this指针做一次offset的调整,具体的内存模型目前上不太清楚,其实理解了感觉用处也不是特别大。