10.4 单件

单件(Singleton)也许是“四人帮”给出的最简单的设计模式,它是允许一个类有且仅有一个实例的方法。下面的程序显示在C++中如何实现一个单件模式:

10.4 单件 - 图1

创建一个单件模式的关键是防止客户程序员获得任何控制其对象生存期的权利。为了做到这一点,声明所有的构造函数为私有,并且防止编译器隐式生成任何构造函数。注意,拷贝构造函数和赋值操作符(这两个方法都故意没有实现,因为它们根本就不会被调用)被声明为私有,以便防止任何这类复制的动作产生。

还必须决定如何去创建这个对象。在这里,它是被静态创建的,但也可以等待,直到客户程序员提出要求再根据要求进行创建。这种方式称作惰性初始化(lazy initialization),这种做法,只在创建对象的代价不大,并且并不总是需要它的情况下才有意义。

如果返回的是一个指针而不是引用,用户可能会不小心删除此指针,因此上述实现被认为是最安全的(析构函数也可以声明为私有或者保护的,以便缓和此问题)。在任何情况下,对象应该私有保存。

通过公有成员函数来提供对其对象的访问。在这里,instance()产生Singleton对象的引用。其余的接口(getValue()和setValue())是常见的类接口。

注意,这种方法并没有限制只创建一个对象。这种技术也支持创建有限个对象的对象池。然而在这种情况下,可能遇到池中共享对象的问题。如果这是一个问题,可以采取创建一个对共享对象进出对象池登记的方法来解决。

单件的变体

一个类中的任何static静态成员对象都表示一个单件:有且仅有一个对象被创建。因此,从某种意义上讲,编程语言对单件技术提供了直接支持;我们自然是在常规基础上使用它。然而,对于static对象(类成员或者非类成员)来说有个问题:就是初始化的顺序的确定,如本书第1卷所述。如果一个静态对象依赖于另一个对象,那么将这些对象按正确的顺序进行初始化是很重要的。

在第1卷中,已经指出了如何在一个函数中定义一个静态对象来控制初始化顺序。这种方法延迟对象的初始化,直到在该函数第1次被调用时才进行初始化。如果该函数返回一个静态对象的引用,就可以达到单件的效果,这样就消除了可能由静态初始化引起的许多烦恼。例如,假如想在第1次调用某个函数时创建一个日志文件,该函数返回了那个日志文件的引用。下面这个头文件将完成这个任务:

10.4 单件 - 图2

函数的实现必须不是内联的(must not be inlined),因为那将意味着整个函数包括在其中定义的静态对象,在任何包含它的翻译单元中都将被复制,这就违反了C++的一次定义(one-definition)规则。[1]这肯定阻碍试图控制初始化顺序的努力(但可能以微妙的、很难发现的形式出现)。因此函数的实现必须分开:

10.4 单件 - 图3

现在log对象不被初始化,直至函数logfile()第1次调用时才被初始化。因此,如果创建一个函数:

10.4 单件 - 图4

在函数的实现中使用logfile():

10.4 单件 - 图5

并且在另一个文件中再次使用logfile():

10.4 单件 - 图6

10.4 单件 - 图7

直至首次调用函数f()时,对象log才被创建。

可以很容易地将在一个成员函数内部的静态对象的创建与单件类结合在一起。SingletonPattern.cpp可用这个方法做如下修改:[2]

10.4 单件 - 图8

如果两个单件彼此依赖,就会产生一个特别有趣的情况,如下所示:

10.4 单件 - 图9

10.4 单件 - 图10

当调用Singleton2:ref()时,它导致其惟一的Singleton2对象被创建。在这个对象的创建过程中,Singleton1:ref()被调用,这导致其惟一的Singleton1对象被创建。因为这种技术不依赖连接或装载的顺序,因此程序员能够很好地控制初始化的全过程,而导致较少的错误。

单件的另外一种变体采用将一个对象的“单件属性(Singleton-ness)”从其实现中分离出来的方法。使用第5章提到的“奇特的递归模板模式(Curiously Recurring Template Pattern)”来实现:

10.4 单件 - 图11

MyClass通过下面3个步骤产生一个单件:

1)声明其构造函数为私有或保护的。

2)声明类Singleton<MyClass>为友元。

3)从Singleton<MyClass>派生出MyClass。

在第3步中的自引用可能令人难以置信,然而正如第5章所述,因为这只是对模板Singleton中模板参数的静态依赖。换句话说,类Singleton<MyClass>的代码之所以能够被编译器实例化,是因为它不依赖于类MyClass的大小。只是在后来,当函数Singleton<MyClass>:instance()第1次被调用时,才需要类MyClass的大小,而此时编译器已经知道类MyClass的大小。[3]

有趣的是,像单件这样简单的设计模式能有多么复杂,这里实际上还没有涉及线程安全的问题。最后说明一点,单件应该少用。真正的单件对象很少出现,而最终单件应该用于代替全局变量。[4]

[1]C++标准要求:“任何翻译单元都不得对任何变量、函数、类类型、枚举类型或模板等多次定义。在程序中使用的非内联函数或对象只能定义一次。”

[2]这被称为Meyers单件,以它的创建者Scott Meyers命名。

[3]在《Modern C++Design》一书中,Andrei Alexandrescu提出了一种优越的基于策略的解决方案实现单件模式。

[4]参看Hyslop和Sutter发表在2003年3月的《issue of CUJ》上的文章“Once is Not Enough”可以了解更详细的信息。