探究C++单例模式

最近又拾起了以前找实习看的东西, 看到了一篇博客写C++单例模式觉得很不错, 但是写的有点点杂乱, 这里我自己再总结一番. 参考的博客链接在这:探究C++单例模式

饿汉模式

饿汉模式是指单例的实例在程序运行的一开始就立即被初始化, 简单代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Singleton
{
public:
    static Singleton& getInstance()
    {
        return m_data;
    }
private:
    static Singleton m_data; //static data member 在类中声明,在类外定义
    Singleton(){}
Singleton(Singleton const&); // copy ctor hidden
Singleton& operator=(Singleton const&); // assign op. hidden
    ~Singleton(){}
};

这个模式有个很明显的问题, 静态成员变量的初始化顺序是无法保证的, 假如有两个单例模式的类ASingleton和BSingleton, 某一天我们打算在BSingleton的构造函数中使用ASingleton的实例, 这个时候, 潜在的问题就出现了, 如果ASingleton的实例在BSingleton使用它之前并没有初始化, 那么ASingleton::getinstance()返回的就是一个未初始化的内存区域, 如果我们要使用这个单例的话, 程序就会不出意外的直接崩溃. 下面给出一个例子来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class ASingleton
{
public:
    static ASingleton* getInstance()
    {
        return &m_data;
    }
    void do_something()
    {
        cout<<"ASingleton do_something!"<<endl;
    }
protected:
    static ASingleton m_data; //static data member 在类中声明,在类外定义
    ASingleton();
ASingleton(ASingleton const&); // copy ctor hidden
ASingleton& operator=(ASingleton const&); // assign op. hidden
    ~ASingleton() {}
};
class BSingleton
{
public:
    static BSingleton* getInstance()
    {
        return &m_data;
    }
    void do_something()
    {
        cout<<"BSingleton do_something!"<<endl;
    }
protected:
    static BSingleton m_data; //static data member 在类中声明,在类外定义
    BSingleton();
BSingleton(BSingleton const&); // copy ctor hidden
BSingleton& operator=(BSingleton const&); // assign op. hidden
    ~BSingleton() {}
};
ASingleton ASingleton::m_data;
BSingleton BSingleton::m_data;
ASingleton::ASingleton()
{
    cout<<"ASingleton constructor!"<<endl;
    BSingleton::getInstance()->do_something();
}
BSingleton::BSingleton()
{
    cout<<"BSingleton constructor!"<<endl;
}

如果我们运行一下的话, 测试的结果大概是这样:
image.png
可以看到这个顺序实际上是有问题的, 因为BSingleton的初始化是在我们使用BSingleton的实例之后的, 但是之所以能打印出来第二行的文字是因为这个函数实际上没有使用类的成员, 也就是说实际上这个函数是不依赖BSingleton的对象的, 在这里我给个简单的例子, 假设有下面一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
using namespace std;
class A
{
public:
void print(){cout<<"hello world"<<endl;}
};
int main()
{
A* a = nullptr;
a->print();
return 0;
}

有兴趣的同学可以跑一下这个代码, 看起来是无法运行的, 但是实际上这个代码是可以正确打印出hello world的. 这个和上面的问题本质上一样的.
到这里, 我们可以很明显的看出来, 实际上饿汗模式的写法是有点问题的, 不过如果我们能确保不在其他单例类的构造函数中使用其他单例, 问题也就不存在了, 下面来介绍一种另外常见的单例模式写法.

懒汉模式

懒汉当然比较懒, 不到万不得已是不会干活的, 这个代码的思想也是来源于此, 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Singleton
{
public:
    static Singleton* getInstance()
    {
        if(! m_data) m_data = new Singleton();
        return m_data;
    }    
private:
    static Singleton* m_data; //static data member 在类中声明,在类外定义
    Singleton(){}
    ~Singleton(){}
};
Singleton* Singleton::m_data = nullptr;

这个实现主要有两个问题, 第一个很明显的问题是析构函数里面什么都没做, 这个问题比较好解决, 在析构函数中加上判断就好, 第二个问题是这个代码是线程不安全的, 也就说在getInstance函数中, 如果两个线程同时进入这个方法中, 那么就会构造出两个变量, m_data也会初始化两次, 所以这样肯定是有问题的.
下面是改进的办法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CSingleton
{
private:
CSingleton() //构造函数是私有的
{
}
CSingleton(const CSingleton &);
CSingleton & operator = (const CSingleton &);
public:
static CSingleton & GetInstance()
{
static CSingleton instance; //局部静态变量
return instance;
}
};

这个基本是没什么问题了, 再考虑到线程安全以及异常安全的话, 有下面这个版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Lock
{
private:
CCriticalSection m_cs;
public:
Lock(CCriticalSection cs) : m_cs(cs)
{
m_cs.Lock();
}
~Lock()
{
m_cs.Unlock();
}
};
class Singleton
{
private:
Singleton();
Singleton(const Singleton &);
Singleton& operator = (const Singleton &);
public:
static Singleton *Instantialize();
static Singleton *pInstance;
static CCriticalSection cs;
};
Singleton* Singleton::pInstance = 0;
Singleton* Singleton::Instantialize()
{
if(pInstance == NULL)
{ //double check
Lock lock(cs); //用lock实现线程安全,用资源管理类,实现异常安全
//使用资源管理类,在抛出异常的时候,资源管理类对象会被析构,析构总是发生的无论是因为异常抛出还是语句块结束。
if(pInstance == NULL)
{
pInstance = new Singleton();
}
}
return pInstance;
}

上面代码稍微值得注意的是在获得Singleton的实例函数中, 有两个判断pInstance是否为nullptr的语句, 第一个是为了减少加锁行为的开销, 第二个是为了线程安全, 这里不再细说.

终极方案

上面其实基本上已经把方案给出来了, 但是下面给出的这个方案更简单, 是boost库中的实现, 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Singleton
{
public:
    static Singleton* getInstance()
    {
        static Singleton instance;
        return &instance;
    }
protected:
    struct Object_Creator
    {
        Object_Creator()
        {
            Singleton::getInstance();
        }
    };
    static Object_Creator _object_creator;

    Singleton() {}
    ~Singleton() {}
};
Singleton::Object_Creator Singleton::_object_creator;

这个方案综合了懒汉模式和饿汗模式的优点, 既使用了类静态成员变量, 也用到了静态局部变量, 这个方案也没有前面两个方案的缺点, 显然这个版本已经没有了线程安全的问题, 因为我们使用了静态局部变量, 对于前面ASingleton和BSingleton的例子, 我们增加下面的测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class ASingleton
{
public:
    static ASingleton* getInstance()
    {
        static ASingleton instance;
        return &instance;
    }
    void do_something()
    {
        cout<<"ASingleton do_something!"<<endl;
    }
protected:
    struct Object_Creator
    {
        Object_Creator()
        {
            ASingleton::getInstance();
        }
    };
    static Object_Creator _object_creator;

    ASingleton();
    ~ASingleton() {}
};
class BSingleton
{
public:
    static BSingleton* getInstance()
    {
        static BSingleton instance;
        return &instance;
    }
    void do_something()
    {
        cout<<"BSingleton do_something!"<<endl;
    }
protected:
    struct Object_Creator
    {
        Object_Creator()
        {
            BSingleton::getInstance();
        }
    };
    static Object_Creator _object_creator;

    BSingleton();
    ~BSingleton() {}
};
ASingleton::Object_Creator ASingleton::_object_creator;
BSingleton::Object_Creator BSingleton::_object_creator;
ASingleton::ASingleton()
{
    cout<<"ASingleton constructor!"<<endl;
    BSingleton::getInstance()->do_something();
}
BSingleton::BSingleton()
{
    cout<<"BSingleton constructor!"<<endl;
}

这次的输出结果就很正常了,如下所示:
image.png
因为在BSingleton::getinstance()的时候初始化了BSingleton的变量, 因此也不存在程序崩掉了的问题了.

总结: 单例模式是一个听起来挺简单, 但是实际上用起来并不简单的东西, 就上面看来, 如果我们只是第一次接触这个模式,大概率我们写出来的单例模式的代码是有问题, 所以这次总结也算是收获颇丰吧.