CPP design pattern Singleton

Singleton模式概念:
保证一个类仅有一个实例,并提供一个访问它的全局访问点。

然而真正实现起来却并非易事,甚至有些棘手。棘手之处在于删除问题,如何删除Singleton实例?谁来删除?何时删除?

在[DP]中,并未探讨有关析构和删除的问题,这是GoF的疏忽。此模式虽小,内涵却多,随着观察的深入,问题便突显出来。之后,John Vlissides(GoF之一)在Pattern hatching(1998)一书中探讨了这个问题。

和工厂模式等不同,Singleton对象需要自己对「所有权」进行管理,用户无权删除实例。

Singleton对象从创建到删除之间便是其生命期,然而,我们只知道创建时间,而不知其删除时间,也就无法管理所有权。

事实上就算我们不进行释放操作,程序在结束之时,操作系统也会将进程所用的内存完全释放,并不会产生内存泄漏。然而,若Singleton产生的实例在构造时申请过广泛的资源(通常是内核资源,如套接字,信号量,互斥体,事件),便会产生资源泄漏。解决办法就是在程序关闭时正确地删除Singleton对象,此时,删除时机便至关重要。

Singleton模式还有一个问题是多线程支持,不过C++11之后并非主要问题。

总结一下,Singleton模式虽然易于表达和理解,但却难以实现。关键在于三点:创建,删除,多线程。其中删除是最棘手的问题,能同时满足这三点的实现,便能适应几乎所有情况。

1 Singleton的唯一性
Singleton模式是一种经过改进的全局变量,重点集中于“产生和管理一个独立对象,并且不允许产生另外一个这样的对象”。

也就是说具有唯一性,因此一切能够再次产生同样对象的方式都应该被杜绝,比如:构造函数,拷贝构造,移动构造,赋值操作符。

[DP]中的定义如下:

 1class Singleton
 2{
 3public:
 4
 5    static Singleton* Instance() {
 6        if(!pInstance_) {
 7            pInstance_ = new Singleton;
 8        }
 9        return pInstance_;
10    }
11
12private:
13    Singleton();
14    Singleton(const Singleton&) = delete;
15    Singleton& operator=(const Singleton&) = delete;
16    Singleton(Singleton&&) = delete;
17    Singleton& operator=(Singleton&&) = delete;
18    ~Singleton();
19
20    static Singleton* pInstance_;
21};
22Singleton* Singleton::pInstance_ = 0;

这种方式的确满足了唯一性,用户除了从Instance()获取对象之外,别无他法。且用户无法意外删除对象,因为析构函数也被私有了(不过依然可以由返回的指针删除之,所以最好以引用返回)。

最大的问题在于此法只满足三大关键之一:创建,对于删除和多线程都不满足。

所以,在满足唯一性的前提下,Singleton的实现还应设法管理产生对象的生命期和多线程环境下的访问安全。

2 隐式析构的Singleton
1节的Singleton只有创建,没有删除,这可能会导致资源泄漏,一种解决方法是使用隐式析构。

C++的static成员变量的生命期伴随着整个程序,在程序关闭时由编译器负责销毁。于是我们可以创建一个static嵌套类,在其析构函数中释放singleton对象。

具体的实现如下:

 1class singleton
 2{
 3public:
 4    static singleton& instance()
 5    {
 6        if(!pInstance_)
 7        {
 8            destroy_.create();
 9            std::cout << "create\n";
10            pInstance_ = new singleton;
11        }
12        return *pInstance_;
13    }
14
15private:
16    static singleton* pInstance_;
17
18    // embedded class
19    // implicit deconstructor
20    struct singleton_destroyer {
21        ~singleton_destroyer() {
22            if(pInstance_) {
23                std::cout << "singleton destroyed!\n";
24                delete pInstance_;
25            }
26        }
27        void create() {}
28    };
29    static singleton_destroyer destroy_;
30
31protected:
32    singleton() = default;
33    virtual ~singleton() {}
34    Singleton(const Singleton&) = delete;
35    Singleton& operator=(const Singleton&) = delete;
36    Singleton(Singleton&&) = delete;
37    Singleton& operator=(Singleton&&) = delete;
38};
39
40singleton* singleton::pInstance_ = nullptr;
41singleton::singleton_destroyer singleton::destroy_;

此时singleton将能够具有摧毁功能,此时再加入线程处理也并非难事,如何保证线程安全可以参见7.5节。

3 Meyers singleton
隐式析构的Singleton的确是一种不错的实现方式,然而C++实现Singleton最简单而有效的方法还是Meyers singleton,实现如下:

 1class singleton
 2{
 3public:
 4    static singleton& instance() {
 5        static singleton obj;
 6        return obj; 
 7    }
 8
 9private:
10    Singleton();
11    Singleton(const Singleton&) = delete;
12    Singleton& operator=(const Singleton&) = delete;
13    Singleton(Singleton&&) = delete;
14    Singleton& operator=(Singleton&&) = delete;
15    ~Singleton();
16};

此法并未使用动态分配和静态指针,而是借用了一个局部静态变量。

和上一个方法一样,该法也是到第一次执行时才会初始化对象(网上有些地方称之为懒汉式,然而在正统C++设计模式相关书籍中,皆未出现过此种叫法),它返回的是引用,所以用户无法对返回的对象进行delete操作(暴力转换法不算:))。此外,在C++11之后,这种方法也是线程安全的,所以对于大多情况下,这是最简单也是最有效的实作法,仅有两行代码。

** Meyers singleton 关键之处是让static对象定义在函数内部,变成局部static变量**

如果是把 static对象定义成 Singleton的私有static成员变量,然后getInstance()去返回这个成员即:

class Singleton {
public:
    static Singleton& getInstance() {
        return inst;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    // 其他数据函数
    // ...

private:
    Singleton() { ... }
    static Singleton inst;
    // 其他数据成员
    // ...
};
Singleton Singleton::inst;

虽然它也是 先getInstance()再访问,但这种不是Meyers’ Singleton!

那么为什么Meyers推荐的是第一种的呢?

原因是这解决了一类重要问题,那就是static变量的初始化顺序的问题。

C++只能保证在同一个文件中声明的static变量的初始化顺序与其变量声明的顺序一致。但是不能保证不同的文件中的static变量的初始化顺序。

然后对于单例模式而言,不同的单例对象之间进行调用也是常见的场景。比如我有一个单例,存储了程序启动时加载的配置文件的内容。另外有一个单例,掌管着一个全局唯一的日志管理器。在日志管理初始化的时候,要通过配置文件的单例对象来获取到某个配置项,实现日志打印。

这时候两个单例在不同文件中各自实现,很有可能在日志管理器的单例使用配置文件单例的时候,配置文件的单例对象是没有被初始化的。这个未初始化可能产生的风险指的是C++变量的未初始化,而不是说配置文件未加载的之类业务逻辑上的未初始化导致的问题。

而Meyers’ Singleton写法中,单例对象是第一次访问的时候(也就是第一次调用getInstance()函数的时候)才初始化的,但也是恰恰因为如此,因而能保证如果没有初始化,在该函数调用的时候,是能完成初始化的。所以先getInstance()再访问 这种形式的单例 其关键并不是在于这个形式。而是在于其内容,局部static变量能保证通过函数来获取static变量的时候,该函数返回的对象是肯定完成了初始化的!

编译器会负责管理局部静态变量,当程序结束时进行析构,看样子一切安好。然而,当生成的多个Singleton对象具有依赖关系时,Meyers singleton就无能为力了(隐式析构Singleton亦如此)。

假设我们有四个Singleton,Director(导演)、Scene(场景)、Layer(图层)、Log(日志)。我们处于一个简单的游戏中,该游戏只有一个导演,一个场景,一个图层,图层上会有若干精灵。我们依次创建,到Layer时初始化失败了,此时创建一个Log来记录崩溃原因。之后程序关闭,执行期的相关机制会来摧毁静态对象,摧毁的顺序是LIFO,所以会先摧毁Log,然后是Layer。但若是Scene关闭失败,此时再想向Log记录崩溃原因,由于Log已被摧毁,所以返回的用只是一个“空壳”,之后的操作便会发生不确定性行为,这种问题称为dead-reference。
dead-reference问题的原因在于C++并不保证静态对象析构的顺序,因此具有依赖关系的多个Singleton无法正确删除。

使用局部静态变量的另一个缺点是难以通过派生子类来扩展Singleton,因为instance创建的始终都是singleton类型的对象。不过可以通过泛型编程来解决,见5节。

4 可以复活的Singleton
要满足具有依赖关系的Singleton,其中一个思路是当需要再次用到已经被执行期处理机制删除的Singleton对象时,使其死灰复燃,复活的Singleton需要自己负责删除。

所以首先需要增加一个标志来记录singleton是否已被摧毁,若已摧毁,则使其复生;若未摧毁,则创建Singleton对象。

大体上的代码如下:

 1class singleton
 2{
 3public:
 4    static singleton& instance()
 5    {
 6        if(!pInstance_)
 7        {
 8            if(destroyed_)
 9                on_dead_reference(); // 出现dead-reference,重生singleton
10            else
11                create(); // 创建singleton
12        }
13        return *pInstance_;
14    }
15private:
16    static singleton *pInstance_;
17    static bool destroyed_; // 记录是否已被摧毁
18};

那么具体就是要看on_dead_reference和create这两个函数是如何处理的,create和之前一样,可以直接使用Meyers singleton:

1static void create()
2{
3    static singleton obj;
4    pInstance = &obj;
5}

当singleton对象析构时,也就是摧毁实例时,改变destroyed_标记(默认为false),将其改为摧毁状态:

1virtual ~singleton()
2{
3    pInstance = nullptr;
4    destroyed_ = true;
5}

而如何让对象死灰复燃呢?我们可以借助placement new,在对象死去的“空壳”上重新创建对象。

1void on_dead_reference()
2{
3    create();
4    new(pInstance_) singleton;
5    std::atexit(kill_singleton);
6    destroyed_ = false;
7}

这里的关键在于atexit函数,该函数可以注册一些函数,注册的函数将会在程序结束之时被调用。事实上static成员变量的默认释放也是通过此函数完成的,编译器会自动生成释放函数向atexit注册,释放机制是LIFO。

此时我们是通过placement new重新构造了singleton对象,所以需要自己释放,因此定义一个kill_singleton函数:

1static void kill_singleton()
2{
3    pInstance_->~singleton();
4}

只需调用析构函数释放便可。

reference: https://mp.weixin.qq.com/s/ufDK34vWC6yTSkS3TmucWw
https://www.zhihu.com/question/56527586
https://github.com/lkimuk/okdp/blob/master/singleton/singleton.hpp

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值