您的位置:首页 > 其它

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

2012-04-13 16:13 302 查看
http://www.windameister.org/blog/2009/01/18/singleton-initialization/Default.aspx?__tencentip=10.4.64.7&__tencentid=1&__tencentrawurl=http://www.windameister.org/blog/2009/01/18/singleton-initialization/

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

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

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

我第一次接触到Singleton模式,是在《Game programming all in one》一书中,该书的示例源代码中采用了以下这种Singleton的实现方式:

class A

{

public:

A()

{

ASSERT(!ms_Singleton);

ms_Singleton = this;

}

static A* GetSingleton() { return ms_Singleton; }

protected:

static A* ms_Singleton;

};

A* A::ms_Singleton = 0;

代码段-1

这种Singleton在使用的时候,需要由使用者进行初始化。并且,如果A类在程序中尚未被实例化之前,其他代码在调用A::GetSingleton的时候,会得到一个空指针。此外,在必要的时候我们可能还需要对类A实例进行显式的释放。

针对Singleton初始化时机的问题,有一种RAII(Resource Acquire Is Initialization)的技术可以保障无论何时,只要我们调用A::GetSingleton,就必然会获取一个有效的A实例指针。

对上述代码段-1我们只需要做少许改动,即可构建一个RAII的Singleton:

class A {

public:

static A* GetSingleton()

{

if (!ms_Singleton)

ms_Singleton = new A;

return ms_Singleton;

}

protected:

A() {}

static A* ms_Singleton;

};

A* A::ms_Singleton = 0;

代码段-2

上述做法相比代码段-1中所展示的做法有一个好处:无论何时我们调用A::GetSingleton,我们都能获得一个有效的指针。而且类A的实例化被类A自身管理,外界不再需要显式构造一个A的实例,与此同时,我们将A的构造函数的访问限制符设置为protected,以防止外界对A的显式构造。

但是,代码段-2中这种做法也存在一个问题,当外界代码不再需要显式构造A的实例时,显式的delete A::ms_Singleton指针的做法非常难堪。虽然我们可以把释放操作放在A的另一个static方法中,但这实际上又是要求我们在程序中的某个地方去显式的调用这一释放函数,与显式的释放内存无异。

上述问题的实质是内存管理问题,解决问题的思路其实也可以从内存管理的角度出发。我们知道程序中的内存分配方式有以下几种:数据段,堆,栈。

代码段-2中的RAII的实现是将A实例化在堆内存中。栈因为其自身特性(仅适合于局部变量的特性),并不适用于Singleton对象的存放需求,那么数据段是否可行呢?

下面一种实现,使用类作用域静态对象,将A的实例存储于数据段上,因为没有将数据置放于堆内存中,因此可以回避上述实现中遇到的内存释放的问题。

class A

{

public:

A() {}

static A* GetSingleton() { return &s_Instance; }

protected:

static A s_Instance;

};

A A::s_Instance;

代码段-3

这种做法是将单件实例用一个全局静态对象保存。全局静态对象会在程序入口点之前被构造。这带来一个好处:我们可以确保程序入口点之后的任何代码访问到该单件对象时,它都已经被构造完毕了。但这种实现方式同时这也带来一个问题:两个全局静态对象的初始化如果存在依赖关系,我们无从确定哪一个会先被构造。

用一个例子来说明,如果我们无法确信两个静态对象中的哪一个会被先初始化,那么我们可能会遇到什么问题:

class MySingleton1

{

public:

MySingleton1() : m_bIsInited(true) { }

static MySingleton1* getSingleton() { return &s_Instance; }

bool IsInited() { return m_bIsInited; }

private:

static MySingleton1 s_Instance;

bool m_bIsInited;

};

MySingleton1 MySingleton1::s_Instance;

class MySingleton2

{

public:

MySingleton2() { m_strBasePath = MySingleton1::getSingleton()->IsInited() ? “C:\” : “”; }

static MySingleton2* getSingleton() { return &s_Instance; }

private:

static MySingleton2 s_Instance;

std::string m_strBasePath;

};

MySingleton2 MySingleton2::s_Instance;

代码段-4

上述代码是一个简单的例子,MySingleton2依赖MySingleton1中的一个状态。以此决定自身初始化的结果。这个例子是虚构的,但是在实际项目中,如果我们采用类似的方法来初始化配置文件以及相关对象的话,那么也同样会遇到类似的问题。MySingleton2必须要在MySingleton1之后被构造,我们如何保障这一点呢?

显然类作用域或全局作用域的静态对象无法为我们提供这种保障。

既然类作用域的静态对象无法为多个Singleton对象相互依赖的情况提供支持,那么函数作用域的方法如何呢?

下面这种Singleton的实现方法,我是从云风老大的《游戏之旅——我的编程感悟》中看到的。对代码段-3的做法也只需要做小小的改动即可:

class A

{

protected:

A() {}

public:

static A* GetSingleton();

};

A* A::GetSingleton()

{

static A _instance;

return &_instance;

}

代码段-5

在GetSingleton()函数中,我们定义了一个函数作用域的静态实例A _instance。由于静态数据会由编译器自动为其在数据段中留存空间,因而我们同样不需要显式的为其分配内存(这里的“显式”是指调用new operator将A实例构造于堆空间中)。而且,前面的依赖问题,在这里不复存在了。由于静态对象被定义在函数作用域当中,因此,该对象会在GetSingleton被第一次访问时构造出来,因此类似于代码段-4中的情况,在这里就不再成为问题了。当MySingleton2对象被构造时,会显式调用MySingleton1::getSingleton函数。此时,如果MySingleton1已经被构造,则直接返回地址,若没有被构造,则会先调用MySingleton1的构造函数,再将其对象地址返回。无论如何,MySingleton2对MySingleton1的依赖都会被正确处理。

为了证明这一点,可以看一下下面这个程序的运行结果:

运行环境是WinXPSP2,编译环境是VC2005。

#include <iostream>

#define CTOR_MSG(className)

cout<<#className<<” Constructed”<<endl

#define DTOR_MSG(className)

cout<<#className<<” Destructed”<<endl

using namespace std;

class Singleton_Impl1 {

protected:

Singleton_Impl1()

: m_bSomeConf(true)

{ CTOR_MSG(Singleton_Impl1); }

public:

~Singleton_Impl1()

{ DTOR_MSG(Singleton_Impl1); }

bool GetSomeConfiguration() { return m_bSomeConf; }

static Singleton_Impl1* getInstance();

private:

bool m_bSomeConf;

};

Singleton_Impl1* Singleton_Impl1::getInstance()

{

static Singleton_Impl1 _instance;

return &_instance;

}

class Singleton_RelyOnImpl1

{

protected:

Singleton_RelyOnImpl1()

{

CTOR_MSG(Singleton_RelyOnImpl1);

if (Singleton_Impl1::getInstance()->GetSomeConfiguration())

cout<<”Do the initialization as Singleton_Impl1′s configuration is true”<<endl;

else

cout<<”Do the initialization as Singleton_Impl1′s configuration is false”<<endl;

}

public:

~Singleton_RelyOnImpl1()

{ DTOR_MSG(Singleton_RelyOnImpl1); }

static Singleton_RelyOnImpl1* getInstance();

};

Singleton_RelyOnImpl1* Singleton_RelyOnImpl1::getInstance()

{

static Singleton_RelyOnImpl1 _instance;

return &_instance;

}

int _tmain(int argc, _TCHAR* argv[])

{

cout<<”Enter Main”<<endl;

Singleton_RelyOnImpl1::getInstance();

cout<<”Leave Main”<<endl;

system(“pause”);

return 0;

}

运行结果如下:

Enter Main

Singleton_RelyOnImpl1 Constructed

Singleton_Impl1 Constructed

Do the initialization as Singleton_Impl1′s configuration is true

Leave Main

可以看到,函数作用域的静态对象在该函数被第一次调用时初始化,并且初始化依赖被正确的处理了。

上面的实现中,当我们每次使用Singleton的时候,都不可避免的要定义一个静态对象或者静态对象指针,以及一个静态类成员函数用以获取单件对象的引用或指针。有没有办法把这些重复的编码消除掉呢?

我们首先想到的就是继承:C++中的继承可以将一些共有的操作和成员放置于基类当中,派生类继承自基类之后,就自然拥有了基类已有的成员函数和成员变量。像这样:

class SingletonBase

{

public:

SingletonBase* getSingleton();

};

class MySingleton1 : public SingletonBase

{

};

现在MySingleton1就继承了来自SingletonBase中的getSingleton()函数。但是,讨厌的事情发生了,我们在使用MySingleton1::getSingleton()时,得到的是一个SingletonBase*的指针。这是件麻烦事,我们不想在获取了指针之后再写讨厌的转型代码,我们希望getSingleton直接返回一个符合我们想要的类型的指针。(MySingleton1::getSingleton()直接返回一个MySingleton1*)。考虑这个需求,我们想到的是:或许可以重载?然而这个念头转眼就被否决了,因为C++无法根据返回值类型进行重载决议。何况,如果我们需要在MySingleton1中写重载函数,那为什么还需要SingletonBase呢?本来在我们的期望中,SingletonBase应该将这些讨厌的事都做了的,我们需要的只是派生一个,然后拿着就用就好~

幸好我们有模板。C++强大的模板功能,为我们提供了这样的可能(以下实现方案的思想来自于Ogre::Singleton的实现,但在具体细节上做了简化。关于Ogre的其他方面的分析文章可以参看这里)。

如果我们将SingletonBase设计为一个模板类,那么上述的头痛问题就迎刃而解了。

template<typename T>

class SingletonBase

{

public:

SingletonBase() { s_pInstance = static_cast<T*>(this); }

~SingletonBase() { assert(s_pInstance); s_pInstance = 0; }

static T* getSingleton() { return s_pInstance; }

protected:

static T* s_pInstance;

};

class MySingleton : public SingletonBase<MySingleton>

{

};

这样一来,我们的MySingleton就立刻拥有了一个static MySingleton* s_pInstance的声明,以及一个static MySIngleton* getSingleton()的定义。

MySingleton的实现文件中还需要给出s_pInstance指针的定义,如下:

template<> MySingleton* SingletonBase<MySingleton>::s_pInstance = NULL;

对于指针s_pInstance的初始化,我们不必操心,因为它在SingletonBase的构造函数中被赋予this的地址。因此只需要构造完成一个MySingleton对象,在其基类部分构造完成时,s_pInstance就已经指向MySingleton的首地址了。(由于早期的编译器在对象内存布局上的一些不符合C++标准的细节,因此在实做中,Ogre在赋值的部分做了一些预编译处理)。

不过这种做法我们虽然摆脱了每次写重复代码的状况,却陷入了必须显式初始化每一个单件对象的境地。

Ogre中是在root对象里对所有的Manager对象(Ogre引擎中的单件对象)进行初始化的。对这类Singleton对象的初始化也非常简单,只需一行代码如下即可:

MySingleton* pMySingleton = new MySingleton;

这样做的好处在于:我们可以完全显式的控制所有的Singleton对象的初始化顺序,而不是像代码段5中的做法一样,将初始化顺序交付给其他代码对Singleton对象的getSingleton函数的调用顺序。

实际上这种做法的本质与代码段1中的做法极其类似,而好处则是:我们不必再写重复代码了。

Singleton的实现有许多种,可能存在许多别的实现方式是本文中没有提及的,针对不同的实现方式,我们应当在实际使用中,根据实际环境以及项目需求,采取合适的做法。

最后,本文也没有涉及在多线程环境下,对Singleton对象的同步问题的说明。事实上,如果Singleton对象被应用在多线程环境中,并应用于跨线程的资源管理,这方面的问题是必须考虑的。

附加:推荐一个wiki链接:http://en.wikipedia.org/wiki/Singleton_pattern

这里对Singleton在各个不同语言环境中的使用做了一个总体概览。其后的Reference等延伸阅读也很有价值。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: