#ant

The dreams in which I'm dying are the best I've ever had...

非完美C++ Singleton实现[1]

Singleton模式是一种非常简单的设计模式,这种模式很常用也很容易被滥用。当你设计应用程序的时候,经常会遇到某些对象在整个程序的生命周期应该仅有一个实例的情况,比如File System,Graphic System,Logging Utility,这时候就可以用到Singleton模式。Singleton模式在GOF中描述如下:

        Ensure a class only has one instance, and provide a global point of access to it.

Singleton模式的定义很简单,实现也有N多种,但是却很难找到一个称得上“完美”的。实现一个完美的Singleton比想象中要难的多,下面探索性的来实现一个非完美的。

1.典型实现
在C++中,Singleton模式的典型实现如下:

 1 // Header file Singleton.h
 2 class  Singleton {
 3 public
:
 4     static Singleton& Instance() { // Unique point of access

 5         if (0 ==  _instance)
 6             _instance = new
 Singleton();
 7         return *
_instance;
 8 
    }
 9     void
 DoSomething();
10 private
:
11     Singleton(); // Prevent clients from creating a new Singleton

12     ~Singleton(); // Prevent clients from deleting a Singleton
13     Singleton(const Singleton&); // Prevent clients from copying a Singleton
14     Singleton& operator=(const Singleton& );
15 private
:
16     static Singleton *_instance; // The one and only instance

17  };
18 

19 // Implementation file Singleton.cpp
20 Singleton* Singleton::_instance = 0;


通过将Singleton的构造函数设为private可以禁止客户代码直接创建Singleton对象,除此之外,Singleton的copy constructor和copy assignment operator都为private且仅有声明没有实现,禁止了客户代码拷贝Singleton对象。唯一可以创建Singleton对象的是Singleton自己的静态成员函数Instance,这样就在编译器保证了Singleton实例的唯一性。上面这些是在C++中实现Singleton模式最基本的要点。

Instance方法保证只有在第一次调用时才会生成Singleton对象,以后的调用只是简单返回唯一的已存在的实例。Instance方法实际上实现的是懒惰初始化(lazy initialize),如果程序中根本没有用到Singleton对象,也就根本不会产生Singleton的实例,这在Singleton对象很少使用且创建Singleton对象开销比较大的情况下特别有用。

客户代码现在可以这样使用Singleton:

1 Singleton &=  Singleton::Instance();
2 s.DoSomething();


还需要说明的是Singleton的析构函数,析构函数也为private可以禁止客户写出如下代码。如果某个客户写出了如下代码,随后的对Singleton的访问就会导致为定义行为,因为Singleton对象已经不存在。

1 Singleton *= & Singleton::Instance();
2 delete p;


2.引入smart pointer
上面的实现算是一个好的实现吗?当然不是,或许连一个正确的实现都算不上。如果你想凑合,当然没问题,上面的代码大多数情况下可以工作的很好。也许你已经注意到了一些问题,比如说在上面的代码中只有new没有delete。是的,你说会发生memory leak对吧,其实memory leak都不是主要的问题,所有的现代操作系统在进程结束的时候都会对内存很好的进行回收。比memory leak更值得让人担忧的是resource leak,如果Singleton在构造函数中请求了某些资源:网络连接,文件句柄,数据库连接等。这些资源将得不到释放。

唯一修正resource leak的方法就是在程序结束的时候delete _instance。当然了,用smart pointer再好不过,在这里用auto_ptr就可以满足需要了(如果你还不知道smart_ptr是什么,花点时间熟悉C++标准库吧),修改后的代码如下:

 1 // Header file Singleton.h
 2 class  Singleton {
 3 public
:
 4     static Singleton& Instance() { // Unique point of access

 5         if (0 ==  _instance.get())
 6             _instance.reset(new
 Singleton());
 7         return *
(_instance.get());
 8 
    }
 9     void
 DoSomething(){}
10 private
:
11     Singleton(){} // Prevent clients from creating a new Singleton

12     ~Singleton(){} // Prevent clients from deleting a Singleton
13     Singleton(const Singleton&); // Prevent clients from copying a Singleton
14     Singleton& operator=(const Singleton& );
15 private
:
16     friend auto_ptr<Singleton>
;
17     static auto_ptr<Singleton> _instance; // The one and only instance

18  };
19 

20 // Implementation file Singleton.cpp
21 auto_ptr<Singleton> Singleton::_instance;


3.用atexit替换smart pointer
C++并没有规定不同编译单元(translation unit,简单说就是一个可编译的cpp文件)中static对象的初始化顺序。如果一个程序中有多个Singleton对象,那么这些Singleton对象的析构顺序也将是任意的。很显然,当多个Singleton对象有依赖关系时,smart pointer根本无法保证Singleton的析构顺序。

msdn中对atexit描述如下:

The atexit function is passed the address of a function (func) to be called when the program terminates normally. Successive calls to atexit create a register of functions that are executed in last-in, first-out (LIFO) order. The functions passed to atexit cannot take parameters. atexit  use the heap to hold the register of functions. Thus, the number of functions that can be registered is limited only by heap memory.

需要说明的是atexit并不比smart pointer好多少,LIFO的保证对于有复杂依赖关系的多个Singleton依然束手无力,但是用atexit替换smart pointer却是必须的,它是设计完美Singleton的基础。


#如果你疑惑atexit为什么还是不行,请考虑下面的情况:
NOTE:下面的情况在Modern C++ Design中叫做KDL(Keyboard,Display,Log)problem

某个程序中使用了如下3个Singleton:KeyboardDisplayLogKeyboardDisplay分别对应于计算机的键盘和显示器,Log用来记录错误信息。假设当KeyboardDisplay的构造函数和析构函数出现错误时会调用Log记录错误信息,并且构造和析构导致的任何错误都会终止程序。

在程序启动时,如果Keyboard构造成功,Display构造失败,很显然在Display的构造函数中将会构造Log而且失败信息会被Log记录,根据假设这时候程序准备退出,atexit注册的函数将会按LIFO的顺序被调用。因为Keyboard先于Log构造,所以Log先于Keyboard析构,但是当由于某种原因Keyboard在析构时失败,想要调用Log记录错误信息时,Log早已被销毁,则Log::Instance()将会导致未定义行为。

#atexit的严重问题:
从上面的例子可以看出,atexit和smart pointer相比仅仅是有LIFO的保证而已,这样的保证貌似也不怎么有效,因为atexit跟smart pointer一样也无法解决KDL probleam

atexit由于LIFO带来了另外的问题,看下面的代码:

 1 #include <cstdlib>
 2 void  Bar() {
 3 
    ...
 4 
}
 5 void
 Foo() {
 6 
    std::atexit(Bar);
 7 
}
 8 int
 main() {
 9 
    std::atexit(Foo);
10     return 0
;
11 }

上面的小段代码用atexit注册了Foo,Foo调用了std::atexit(Bar)。当程序退出时,根据atexit的LIFO保证,Bar在Foo之后注册,因此Bar应该在Foo之前调用,但是当Bar注册的时候Foo已经调用了,Bar根本就没有机会能够在Foo之前调用。这明显自相矛盾对吧,没办法,C++标准好像忽视了这一点,因此如果类似代码被调用,肯定不会有什么好的结果,好一点是resource leak,差一点估计程序就崩溃了!!!

atexit的这个问题跟Singleton有关系吗?当然有,如果在一个Singleton的析构函数中调用atexit就会出现上述问题。即在KDL problem中,如果KeyboardDisplay都构造成功,当KeyboardDisplay任意一个析构失败时,KeyboardDisplay在析构函数中会构造LogLog的构造函数会间接调用atexit。oops!!!,可怕的未定义行为。

看到这里你一定对atexit相当失望,貌似它带来的好处多于坏处。但是请你相信,如果适当设计,atexit在后面的Singleton改造中会起到很重要的作用。

用atexit后的代码:

 1 // Header file Singleton.h
 2 class  Singleton {
 3 public
:
 4     static Singleton& Instance() { // Unique point of access

 5         if (0 ==  _instance) {
 6             _instance = new
 Singleton();
 7             atexit(Destroy); // Register Destroy function

 8          }
 9         return *
_instance;
10 
    }
11     void
 DoSomething(){}
12 private
:
13     static void Destroy() { // Destroy the only instance

14         if ( _instance != 0  ) {
15 
            delete _instance;
16             _instance = 0
;
17 
        }
18 
    }
19     Singleton(){} // Prevent clients from creating a new Singleton

20     ~Singleton(){} // Prevent clients from deleting a Singleton
21     Singleton(const Singleton&); // Prevent clients from copying a Singleton
22     Singleton& operator=(const Singleton& );
23 private
:
24     static Singleton *_instance; // The one and only instance

25  };
26 

27 // Implementation file Singleton.cpp
28 Singleton* Singleton::_instance = 0;

你有没有仔细考虑过Destroy中的_instance = 0;这一行代码,上述代码实际上实现的是不死鸟模式(The Phoenix Singleton),所谓不死鸟,就跟一辉一样可以死而复生。上面的代码可以解决本文最早提出的KDL problem,即如果Keyboard析构失败,虽然Log已经析构,但是由于Destroy中的_instance = 0;这一行代码,Log::Instance()将会创建一个新的Log对象,程序将会表现良好。当然了,Phoenix Singleton仅能用于无状态的Singleton,如果Log需要保存某些状态,Phoenix Singleton也不会带来任何好处。你当然可以用某些方法维持Phoenix Singleton的状态,但是在做之前先想想看是否值得,维持状态可能会使Singleton变得特别复杂。

上面的Phoenix Singleton已经可以满足大部分需要,如果你的Singleton没有涉及到多线程,多个Singleton之间也没有依赖关系,你大可以放心使用。但是如果你用到多线程,或者你的Singleton关系如KDL般复杂,或者你觉得对每一个Singleton都敲同样的代码让你厌烦。在后面几篇会有一个多线程安全的,能够解决多个Singleton依赖关系的,基于模板的Singleton实现。

posted on 2007-09-07 14:49 蚂蚁终结者 阅读(4983) 评论(12)  编辑 收藏 引用 所属分类: Design Pattern

Feedback

# re: 非完美C++ Singleton实现[1] 2007-09-07 15:04 5Element

不错,期待下一篇。  回复  更多评论   

# re: 非完美C++ Singleton实现[1] 2007-09-07 15:53 DeathKnight

先赞一下你的文字排版 很漂亮  回复  更多评论   

# re: 非完美C++ Singleton实现[1][未登录] 2007-09-07 16:22 L

这个Singleton没有加锁,多线程下会有问题。  回复  更多评论   

# re: 非完美C++ Singleton实现[1] 2007-09-07 16:36 蚂蚁终结者

@L
恩,其实还有很多问题,后面几篇会解决  回复  更多评论   

# re: 非完美C++ Singleton实现[1] 2007-09-07 17:32 bgate

最起码应该写成个模板类吧.  回复  更多评论   

# re: 非完美C++ Singleton实现[1] 2007-09-07 17:36 Minidx全文检索

呵呵,的确是“非完美”的,不过支持一下~~~  回复  更多评论   

# re: 非完美C++ Singleton实现[1] 2007-09-07 19:29 蚂蚁终结者

大家都没有看到我最后一句话:
在后面几篇会有一个多线程安全的,能够解决多个Singleton依赖关系的,基于模板的Singleton实现。

由于篇幅比较长,所以分成好几篇了,thanks!  回复  更多评论   

# re: 非完美C++ Singleton实现[1][未登录] 2007-09-07 20:49 杨粼波

看看Loki里面的实现。  回复  更多评论   

# re: 非完美C++ Singleton实现[1] 2007-09-08 09:10 蚂蚁终结者

@杨粼波
Loki的我看过,不过感觉太复杂,也许有时候需要的只是最简单的  回复  更多评论   

# re: 非完美C++ Singleton实现[1] 2007-09-09 00:12 攀升

不知道能不能写一个原型模式的文章,我最近想用  回复  更多评论   

# re: 非完美C++ Singleton实现[1] 2007-09-09 09:30 蚂蚁终结者

@攀升
“原型模式”目前还没有研究过,如果最近有时间的话可能会写一篇  回复  更多评论   

# re: 非完美C++ Singleton实现[1] 2007-09-10 09:45 Uranus

谢谢,  回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理