【GOF设计模式之路】-- Singleton

来源:互联网 发布:java获取局域网ip地址 编辑:程序博客网 时间:2024/06/02 00:06

之前一直徘徊第一篇该写哪一种设计模式,最后决定还是以Singleton模式开始吧。之所以以它开始,原因在我于个人认为,相对来说它在设计上比较单一,比较简单一些。在通常情况下,它是最容易理解的。同样也正因为它容易理解,细节才更值得注意,越是简单的东西,往往会被我们忽略一些细节。关于Singleton模式的讨论和实现也非常的多,GOF设计模式也只对它进行了简单的描述,本文则打算相对全面的介绍一下Singleton模式,目的在于挖掘设计的思想。

Singleton模式又称单件模式,我们都知道全局变量,Singleton就相当于一个全局变量,只不过它是一种被改进的全局变量。体现在哪儿呢?普通的全局变量可以有多个,例如某个类,可以定义很多个这个类的全局对象,可以分别服务于整个应用程序的各个模块。而Singleton则只允许创建一个对象,就好比不允许有克隆人一样,哪天在大街上看见一个一模一样的你,你会是什么感受?从计算机的角度,例如键盘、显示器、系统时钟等,都应该是Singleton,如果有多个这样的对象存在,很可能带来危险,而这些危险却不能换来切实的好处。

在GOF设计模式里,对Singleton的描述很简单:“保证一个类只有一个实体(instance),并为其提供一个全局访问点(global access point)”。所谓全局访问点,就是Singleton类的一个公共全局的访问接口,用于获得Singleton实体,对于用户来说,只需要简单的步骤就能获得这个实体,与全局变量的访问一样简单,而Singleton对象的创建与销毁有Singleton类自己承担,用户不必操心。既然是自己管理,我们就得将其管理好,让用户放心,现在是提倡服务质量的社会,我们应该有服务的态度。

理论讲了一大堆,迫不及待想看看具体实现,先看一个初期的版本:

/* Singleton.h */

//-----------------------------------------------------------------------------------
//  Desc:   Singleton Header File
//  Author: masefee
//  Date:   2010.09
//  Copyright (C) 2010 masefee
//-----------------------------------------------------------------------------------

#ifndef __SINGLETON_H__
#define __SINGLETON_H__

class Singleton
{
public:
    static Singleton* Instance( void );

public:
    int  doSomething( void );

protected:
    Singleton( void ){}

private:
    static Singleton* ms_pInstance;
};

#endif

/* Singleton.cpp */

//-----------------------------------------------------------------------------------
//  Desc:   Singleton Source File
//  Author: masefee
//  Date:   2010.09
//  Copyright (C) 2010 masefee
//-----------------------------------------------------------------------------------

#include "Singleton.h"

Singleton* Singleton::ms_pInstance = 0;

Singleton* Singleton::Instance( void )
{
    if ( ms_pInstance == 0 )
        ms_pInstance = new Singleton;

    return ms_pInstance;
}

int  Singleton::doSomething( void )
{

}

/* main.cpp */

#include "Singleton.h"

int main( void )
{
    Singleton* sig = Singleton::Instance();
    sig->doSomething();
    delete sig;

    return 0;
}

Singleton类的构造函数被声明为protected,当然也可以声明为private,声明为protected是为了能够被继承。但用户不能自己产生Singleton对象,唯一能产生Singleton对象的就只有Instance成员函数,这个Instance函数即所谓的全局访问点。访问方式如上面main函数中的红色代码。如果用户没有调用Instance函数,Singleton对象就不会产生出来,这样优化的成本是Instance函数里的if检测,但是好处是,如果Singleton对象产生很昂贵,而本身有很少使用,这种“使用才诞生”的方案就会显尽优势了。

上面将ms_pInstance在全局初始化为0,这样做有个好处,即编译器在编译时就已经将ms_pInstance的初始值写到了可执行二进制文件里了,Singleton对象的唯一性在这个时期就已经决定了,这也正是C++实现Singleton模式的精髓所在。如果将ms_pInstance改造一下,如:

class Singleton
{
public:
    static Singleton* Instance( void );

public:
    void doSomething( void );

protected:
    Singleton( void ){}

private:
    static Singleton ms_Instance;
};

Singleton Singleton::ms_Instance;

Singleton* Singleton::Instance( void )
{
       return &ms_Instance;
}

将ms_pInstance由指针改成了对象,这样做未必是件好事,原因在于ms_Instance是被动态初始化(在程序运行期间调用构造函数进行初始化)的,而ms_pInstance在前面已经说过,它是属于静态初始化,编译器在编译时就将常量写入到二进制文件里,在程序装载到内存时,就会被初始化。我们都知道,在进入main之前,有很多初始化操作,对于不同编译单元的动态初始化对象,C++并没有规定其初始化顺序,因此上面的改造方法存在如下隐患:

#include "Singleton.h"

int g_iRetVal = Singleton::Instance()->doSomething();

由于无法确保编译器一定先将ms_Instance对象初始化,所以全局变量g_iRetVal在被初始赋值时,Singleton::Instance()调用可能返回一个尚未构造的对象,这也就意味着你无法保证任何外部的对象使用的ms_Instance对象都是一个已经被正确初始化的对象。危险也就不言而喻了。

Singleton模式意为单件模式,于是保证其唯一性就成了关键,看看上面的第一种实现,ms_pInstance虽然是Instance成员函数所创建,它返回了一个Singleton对象的指针给外界,并且将这个指针的销毁权利赋予了外界,这就存在了第一个隐患,倘若外界将返回的指针给销毁了,然后再重新调用Instance函数,则前后对象内存地址通常将发生变化,如:

Singleton* sig1 = Singleton::Instance();
sig->doSomething();
delete sig;

Singleton* sig2 = Singleton::Instance();

如上,sig1和sig2的指向的Singleton对象的内存地址通常是不一样的,例如前面的sig1指向的对象保存了一些状态,这样销毁之后再次创建,状态已经被清除,程序也就容易出错了。所以为了避免这样类似的情况发生,我们再改进一下Instance函数:

static Singleton& Instance( void );

传回引用则不用担心被用户释放掉对象了,这样就比较安全了,Singleton对象都由其自身管理。

在C++类中,还有一个copy(复制)构造函数,在上面的Singleton类里,我们并没有显示声明copy构造,于是如果有以下写法:

Singleton  sig( Singleton::Instance() );

如果我们不显示声明一个copy构造,编译器会帮你生成一个默认的public版本的copy构造。上面的写法就会调用默认的copy构造,从而用户就能在外部声明一个Singleton对象了,这样就存在了第二个隐患。因此,我们将copy构造也声明为protected保护成员。

另外还有一个成员函数,赋值(assignment)操作符。因为你不能将一个Singleton对象赋值给另外一个Singleton对象,这违背了唯一性,不允许存在两个Singleton对象。因此我们将赋值操作符也声明为保护成员,同时对于Singleton来说,它的赋值没有意义,唯一性的原则就使它只能赋值给自己,所以赋值操作符我们不用去具体实现。

最后一个是析构函数,如前面所说,用户会在外界释放掉Singleton对象,为了避免这一点,所以我们也将析构函数声明为保护成员,就不会意外被释放了。

上述所有手段统一到一起之后,Singleton类的接口声明如下:

class Singleton
{
public:
    static Singleton& Instance( void );

public:
    void doSomething( void );
   

protected:
    Singleton( void );
    Singleton( const Singleton& other );
    ~Singleton( void );
    Singleton& operator =( const Singleton& other );

private:
    static Singleton* ms_pInstance;
};

这样似乎已经完美了,Singleton对象的创建完全有Singleton类自身负责了,再看前面的创建过程,ms_pInstance是一个指针,Singleton对象是动态分配(new)出来的,那么释放过程就得我们手工调用delete,否则将发生内存泄露。然而析构函数又被我们定义为保护成员了,因此析构问题还没有得到解决。

这成了一个比较棘手的问题,既要保证程序运行时整个范围的唯一性,又要保证在销毁Singleton对象时没有人在使用它,所以销毁的时机显得尤为重要,也比较难把握。

于是有人想到了一个比较简单的方案,不动态分配Singleton对象便可以自动销毁了,但销毁的最好时期是在程序结束时最好,于是想到了如下方案:

Singleton& Singleton::Instance( void )
{
    static Singleton _inst;
    return _inst;
}

_inst是一个静态的局部变量,它的初始化是在第一次进入Instance函数时,这属于执行期初始化,而与编译期间常量初始化不同,_inst对象初始化要调用构造函数,这不可能在编译期间完成,与:

int func( void )
{
    static int a = 100;  // 编译期间常量初始化
    return a;
}

不同。a的值在编译期间就已经决定了,在应用程序装载到内存时就已经为100了,而非在第一次执行func函数时才被赋值为100。

_inst对象的销毁工作由编译器承担,编译器将_inst对象的销毁过程注册到atexit,它是一个标准C语言库函数,让你注册一些函数得以在程序结束时调用,调用次序与栈操作类似,后进先出的原则。atexit的原型:

int __cdecl atexit( void ( __cdecl* pFunc )( void ) );

在Instance函数的反汇编代码上有所体现(VS2008 Release 禁用优化(/Od)):

Singleton& Singleton::Instance( void )
{
00CB1040  push        ebp 
00CB1041  mov         ebp,esp
    static Singleton _inst;
00CB1043  mov         eax,dword ptr [$S1 (0CB3374h)]
00CB1048  and         eax,1
00CB104B  jne         Singleton::Instance+33h (0CB1073h)
00CB104D  mov         ecx,dword ptr [$S1 (0CB3374h)]
00CB1053  or          ecx,1
00CB1056  mov         dword ptr [$S1 (0CB3374h)],ecx
00CB105C  mov         ecx,offset _inst (0CB3370h)
00CB1061  call        Singleton::Singleton (0CB1020h)
00CB1066  push        offset `Singleton::Instance'::`2'::`dynamic atexit destructorfor '_inst'' (0CB1880h)
00CB106B  call        atexit (0CB112Eh)
00CB1070  add         esp,4
    return _inst;
00CB1073  mov         eax,offset _inst (0CB3370h)
}
00CB1078  pop         ebp 
00CB1079  ret   

红色的一句汇编代码即是得到_inst的析构过程地址压入到atexit的参数列表,红色粗体则调用了atexit函数注册这个析构过程。这里所谓的析构过程并不是Singleton类的析构函数,而是如下过程:

`Singleton::Instance'::`2'::`dynamic atexit destructor for '_inst'':
00CB1880  push        ebp 
00CB1881  mov         ebp,esp
00CB1883  mov         ecx,offset _inst (0CB3370h)
00CB1888  call        Singleton::~Singleton (0CB1030h)
00CB188D  pop         ebp 
00CB188E  ret

这也是一个函数,在此函数里再调用Singleton的析构函数,如蓝色那句汇编代码。道理很简单,由于Singleton的析构函数是__thiscall,需要传递类对象,所以不是直接call析构函数的地址。

这种方式销毁在大多数情况下是有效的,在实际中,这种方式也用得比较多。可以根据实际的情况,选择不同的机制,Singleton没有定死只能用哪种方式。

既然上述方式在大多数情况下是有效的,那么肯定就有一些情况会有问题,这就引出了KDLkeyboard、display、log问题,假设我们程序中有三个singletons:keyboard、display、log,keyboard和display表示真实的物体,log表示日志记录,可以是输出到屏幕或者记录到文件。而且log由于创建过程有一定的开销,因此在有错误时才会被创建,如果程序一直没有错误,则log将不会被创建。

假如程序开始执行,keyboard顺利创建成功,而display创建过程中出现错误,这是需要产生一条log记录,log也就被创建了。这时由于display创建失败了,程序需要退出,由于atexit是后注册的先调用,log最后创建,则也是最后注册atexit的,因此log最先销毁,这没有问题。但是log销毁了,如果随后的keyboard如果销毁失败需要产生一条log记录,而这是log已经销毁了,log::Instance会不明事理的返回一个引用,指向了一个log对象的空壳,此后程序便不能确定其行为了,很可能发生其他的错误,这也称之为"dead - reference"问题。

从上面的分析来看,我们是想要log最后销毁,不管它是在什么时候创建的,都得在keyboard和display之后销毁,这样才能记录它们的析构过程中发生的错误。于是我们又想到,可以通过记录一个状态,来作为"dead - reference"检测。例如定义一个static bool ms_bDestroyed变量来标记Singleton是否已经被销毁。如果已经销毁则置为true,反之置为false。

/* Singleton.h */

class Singleton
{
public:
    static Singleton& Instance( void );

public:
    void doSomething( void );

protected:
    Singleton( void ){};
    Singleton( const Singleton& other );
    ~Singleton( void );
    Singleton& operator =( const Singleton& other );  

private:
    static Singleton* ms_pInstance;
    static bool           ms_bDestroyed;
};

/* Singleton.cpp */

#include <iostream>
#include "Singleton.h"

Singleton* Singleton::ms_pInstance  = 0;
bool       Singleton::ms_bDestroyed = false;

Singleton& Singleton::Instance( void )
{
    if ( !ms_pInstance )
    {
        if ( ms_bDestroyed )
            throw std::runtime_error( "Dead Reference Detected" );
        else
        {
            static Singleton _inst;
            ms_pInstance = &_inst;
        }
    }
    return *ms_pInstance;
}

Singleton::~Singleton( void )
{
    ms_pInstance  = 0;
    ms_bDestroyed = true;
}

void Singleton::doSomething( void )
{

}

这种方案能够准确的检测"dead - reference",如果Singleton已经被销毁,ms_bDestroyed成员被置为true,再次获取Singleton对象时,则会抛出一个std::runtime_error异常,避免程序存在不确定行为。这种方案相对来说比较高效简洁了,也可适用于一定场合。

但这种方案在有的时候也不能让我们满意,虽然抛出了异常,但是KDL问题还是没有被最终解决,只是规避了不确定行为。于是我们又想到了一种方案,即是让log重生,一旦发现log被销毁了,而又需要记录log,则再次创建log。这就能保证log至始至终一直存在了,我们只需要在Singleton类里添加一个新的成员函数:

class Singleton
{
    ... ... other member ... ...

private:
    static void destroySingleton( void );
};

实现则为:

void Singleton::destroySingleton( void )
{
    ms_pInstance->~Singleton();
}

Instance成员就得在改造一下了:

Singleton& Singleton::Instance( void )
{
    if ( !ms_pInstance )
    {
        static Singleton _inst;
        ms_pInstance = &_inst;

        if ( ms_bDestroyed )
        {
            new( ms_pInstance ) Singleton;
            atexit( destroySingleton );
            ms_bDestroyed = false;
        }
    }
    return *ms_pInstance;
}

destroySingleton与前面汇编那段相似,相当于这个工作让我们自己来做了,而不是让编译器来做。destroySingleton手动调用Singleton的析构函数,而destroySingleton又被我们注册到atexit。当ms_pDestroyed为真时,则再次在ms_pInstance指向的内存出创建一个新的Singleton对象,这里使用的是placement new操作符,它并不会新开辟内存(参见:利用C++的operator new实现同一对象多次调用构造函数)。之后,注册destroySingleton为atexit,在程序结束时调用并析构Singleton对象。如此而来,便保证了Singleton对象的生命期跨越整个应用程序。log如果作为这样一个Singleton对象,那么无论log在什么时候被销毁,都能记录所有错误日志了。

似乎到此已经就非常完美了,但是还有一点不得不提,使用atexit具有一个未定义行为:

void func1( void )
{

}

void func2( void )
{
    atexit( func1 );
}

void func3( void )
{

}

int main( void )
{
    atexit( func2 );
    atexit( func3 );
    return 0;
}

CC++标准里并没有规定上面这种情况的执行次序,按前面的后注册先执行的说法,按理说func1被最后注册,则应该最先执行它,但是它的注册是由func2负责的,这就得先执行func2,才能注册func1。这样就产生了矛盾,所以以编译器的次序为准,在VS2008下,上面的例子中,最先执行func3,再执行func2,最后执行func1。看起来像是一个层级关系,main函数的注册顺序是一层,后来的又是一层,也可以认为在被注册为atexit的函数里再注册其它函数时,其它函数的执行次序在当前函数之后,如果注册了多个,则最后注册的在当前函数执行后立即执行。

上面这种机制是通过延长Singleton的声明周期,它破坏了其正常的生命周期。很可能带来不必要的迷惑,于是我们又想到了一种机制:是否能够控制Singleton的寿命呢,让log的寿命比keyboard和display的寿命长,便能够解决KDL问题了。控制寿命还可以针对不同的对象,不单单只是Singleton,它可以说是一种可以移植的概念。

我们实现一个生命期管理器,如下:

view plaincopy to clipboardprint?
  1. ---LifeTime.hpp-- 
  2. --由于代码编辑器的问题,省略了include-- 
  3. class BaseLifetimeTracker 
  4. public
  5.     BaseLifetimeTracker( unsigned int longevity ) 
  6.         : m_iLongevity( longevity ) 
  7.     { 
  8.          
  9.     } 
  10.  
  11.     virtual ~BaseLifetimeTracker( void
  12.     { 
  13.  
  14.     } 
  15.  
  16.     friend inlinebool Compare( const BaseLifetimeTracker* x,  
  17.                                 const BaseLifetimeTracker* y ) 
  18.     { 
  19.         return y->m_iLongevity < x->m_iLongevity; 
  20.     } 
  21.  
  22. protected
  23.     static unsigned int elements;  
  24.     static BaseLifetimeTracker** pTrackerArray; 
  25.  
  26. private
  27.     unsigned int m_iLongevity; 
  28. }; 
  29.  
  30. unsigned int BaseLifetimeTracker::elements = 0;  
  31. BaseLifetimeTracker** BaseLifetimeTracker::pTrackerArray = 0; 
  32.  
  33. template< class T >  
  34. class Deleter 
  35. public
  36.     static void Delete( T* pObj ) 
  37.     { 
  38.         delete pObj; 
  39.     } 
  40. }; 
  41.  
  42. template< class T,class Destroyer = Deleter< T > >  
  43. class LifetimeTracker :  
  44.     public BaseLifetimeTracker 
  45. public
  46.     static void SetLongevity( T* pDynObj, unsignedint longenvity ) 
  47.     { 
  48.         BaseLifetimeTracker** pNewArray = static_cast< BaseLifetimeTracker** >( 
  49.             std::realloc( pTrackerArray, sizeof( BaseLifetimeTracker* ) * ( elements + 1 ) ) ); 
  50.  
  51.         if ( !pNewArray ) 
  52.             throw std::bad_alloc(); 
  53.  
  54.         pTrackerArray = pNewArray; 
  55.  
  56.         BaseLifetimeTracker* pInsert = new LifetimeTracker< T >( pDynObj, longenvity ); 
  57.         BaseLifetimeTracker** pos = std::upper_bound( pTrackerArray, pTrackerArray + elements, pInsert, Compare ); 
  58.  
  59.         std::copy_backward( pos, pTrackerArray + elements, pTrackerArray + elements + 1 ); 
  60.  
  61.         *pos = pInsert; 
  62.         ++elements; 
  63.          
  64.         std::atexit( AtExitFn ); 
  65.     } 
  66.  
  67.     static void AtExitFn(void
  68.     { 
  69.         assert( elements > 0 && pTrackerArray != 0 ); 
  70.         BaseLifetimeTracker* pTop = pTrackerArray[ elements - 1 ]; 
  71.         pTrackerArray = static_cast< BaseLifetimeTracker** >( 
  72.             std::realloc( pTrackerArray, sizeof( BaseLifetimeTracker* ) * --elements ) ); 
  73.  
  74.         delete pTop; 
  75.     } 
  76.  
  77. public
  78.     LifetimeTracker( T* pDynObj, unsigned int longevity ) 
  79.         : BaseLifetimeTracker( longevity ), m_pTracked( pDynObj ) 
  80.     { 
  81.     } 
  82.  
  83.     ~LifetimeTracker( void
  84.     { 
  85.         Destroyer::Delete( m_pTracked ); 
  86.     } 
  87.  
  88. private
  89.     T*        m_pTracked; 
  90. }; 
 

有了这个管理器,就能设置Singleton对象的寿命了,pTrackerArray是按生命周期的长度进行升序排列的,最前面的就是最先销毁的,这与前面的销毁规则是一致的。 对于前面的KDL问题,我们就可以将keyboard和display的寿命设置为1,log的寿命设置为2,keyboard和display不存在先后问题,寿命相同也不影响。log寿命为2,大于keyboard和display就行,保证在最后销毁。这样一来,在单个线程下的KDL问题就完美解决了。

既然上面说了是在单线程里,言外之意就会存在多线程问题,例如:

Singleton& Singleton::Instance( void )
{
    if ( !ms_pInstance )
    {
        ms_pInstance = new Singleton;
    }
    return *ms_pInstance;
}

假如有两个线程要访问这个Instance,第一个线程进入Instance函数,并检测if条件,由于是第一次进入,if条件成立,进入了if,执行到红色代码。此时,有可能被OS的调度器中断,而将控制权交给另外一个线程。

第二个线程同样来到if条件,发现ms_pInstance还是为NULL,因为第一个线程还没来得及构造它就已经被中断了。此时假设第二个线程完成了new的调用,成功的构造了Singleton,并顺利的返回。

很不幸,第一个线程此刻苏醒了,由于它被中断在红色那句代码,唤醒之后,继续执行,调用new再次构造了Singleton,这样一来,两个线程就构建两个Singleton,这就破坏了唯一性。

我们意识到,这是一个竞态条件问题,在共享的全局资源对竞态条件和多线程环境而言都是不可靠的。怎么避免上面的这种情况呢,有一种简单的做法是:

Singleton& Singleton::Instance( void )
{
    _Lock holder( _mutex );
    if ( !ms_pInstance )
    {
        ms_pInstance = new Singleton;
    }
    return *ms_pInstance;
}

_mutex是一个互斥体,_Lock类专门用于管理互斥体,在_Lock的构造函数中对_mutex加锁,在析构函数中解锁。这样保证同一在锁定之后操作不会被其它线程打断。holder是一个临时的_Lock对象,在Instance函数结束时会调用其析构,自动解锁。这也是著名的RAII机制。

似乎这样做确实能够解决竞态条件的问题,在一些场合也是可以的,但是在需要更高效率的环境下,这样做缺乏效率,比起简单的if ( !ms_pInstance )测试要昂贵很多。因为每次进入Instance函数都加锁解锁一次,即使需要加锁解锁的只有第一次进入时。所以我们想要有这样的解法:

Singleton& Singleton::Instance( void )
{
    if ( !ms_pInstance )
    {
        _Lock holder( _mutex );
        ms_pInstance = new Singleton;
    }
    return *ms_pInstance;
}

这样虽然解决了效率问题,但是竞态条件问题又回来了,打不到我们的要求,因为两个线程都进入了if,再锁定还是会产生两个Singleton对象。于是有一个比较巧妙的用法,即“双检测锁定”Double-Checked Locking模式。直接看效果吧:

Singleton& Singleton::Instance( void )
{
    if ( !ms_pInstance )
    {
        _Lock holder( _mutex );
        if ( !ms_pInstance )
            ms_pInstance = new Singleton;
    }
    return *ms_pInstance;
}

非常美妙,这样就解决了效率问题,同时还解决了竞态条件问题。即使两个线程都进入了第一个if,但第二个if只会有一个线程进入,这样当某个线程构造了Singleton对象,其它线程因为中断在_Lock holder( _mutex )这一句。等到唤醒时,ms_pInstance已经被构造了,第二个if测试就会失败,便不会再次创建Singleton了。第一个if显得很粗糙快速,第二个if显得清晰缓慢,第一个if是为了第二次进入Instance函数提高效率不再加锁,第二个if是为了第一次进入Instance避免产生多个Singleton对象,各施其职,简单而看似多余的if测试改进,显得如此美妙。

本文从开头到现在,一次又一次感到完美,又一次一次发现不足。到此,又似乎感到了完美,但完美背后还真容易有阴霾。虽然上面的双检测锁定已经在理论上胜任了这一切,趋近于完美。但是,有经验的程序员,会发现它还是存在一个问题。

对于RISC(精简指令集)机器的编译器,有一种优化策略,这个策略会将编译器产生出来的汇编指令重新排列,使代码能够最佳运用RISC处理器的平行特性(可以同时执行几个动作)。这样做的好处是能够提高运行效率,甚至可以加倍。但是不好之处就是破坏了我们的“完美”设计“双检测锁定”。编译器很可能将第二个if测试的指令排列到_Lock holder( _mutex )的指令之前。这样竞态条件问题又出现了,哎!

碰到这样的问题,就只有翻翻编译器的说明文档了,另外可以在ms_pInstance前加上volatile修饰,因为合理的编译器会为volatile对象产生恰当而明确的代码。

到此,常见的问题都基本解决了,不管是多线程还是单线程,在具体的环境我们再斟酌选择哪一种方式,因此,本文并没有给出一个统一的解决方案。你还可以将上面的机制组装到一起,写成一个SingletonHolder模板类,在此就不实现了。Singleton还能根据具体进行扩展,方法也不止上面这些,我们只有一个目的,让它正确的为我们服务。

在本文开头说Singleton是一个相对好理解的一种设计模式,但从整篇下来,它也并不是那么单纯。由简单到复杂,每一种设计方案都有它的用武之地,例如,我们的程序里根本就不会出现KDL问题,那么就可以简单处理。再者我们有的Singleton不可能在多线程环境里运行,那么我们也没有必要设计多线程这一块,而只需要在考虑问题时意识到就可以了。做到一切尽在掌握之中即可。

好吧!本文就到此结束,重在体会这些细节的机制和挖掘问题然后解决问题的乐趣。在此感谢《Modern C++ Design》,望大家多提意见,感谢!!

原创粉丝点击