探究 C++ Singleton(单例模式)

时间:2021-12-14 20:49:01
  一、静态化并不是单例模式
  初学者可能会犯的错误, 误以为把所有的成员变量和成员方法都用 static 修饰后, 就是单例模式了:
class Singleton
{
public:
    /* static method */
    
private:
    static Singleton m_data; //static data member 在类中声明,在类外定义
};

Singleton Singleton::m_data;
         乍一看确实具备单例模式的很多条件, 不过它也有一些问题. 第一, 静态成员变量初始化顺序不依赖构造函数, 得看编译器心情的, 没法保证初始化顺序 (极端情况: 有 a b 两个成员对象, b 需要把 a 作为初始化参数传入, 你的类就 必须 得要有构造函数, 并确保初始化顺序).
  第二, 最严重的问题, 失去了面对对象的重要特性 -- "多态", 静态成员方法不可能是 virtual 的(补充一点,静态成员方法也不可能是 const 的. Singleton类的子类没法享受 "多态" 带来的便利.

  二、饿汉模式
  饿汉模式 是指单例实例在程序运行时被立即执行初始化:

class Singleton
{
public:
    static Singleton& getInstance()
    {
        return m_data;
    }
    
private:
    static Singleton m_data; //static data member 在类中声明,在类外定义
    Singleton(){}
    ~Singleton(){}
};

Singleton Singleton::m_data;
  
    这种模式的问题也很明显, 类现在是多态的, 但静态成员变量初始化顺序还是没保证: 假如有两个单例模式的类 ASingleton 和 BSingleton, 某天你想在 BSingleton 的构造函数中使用 ASingleton 实例, 这就出问题了. 因为 BSingleton m_data 静态对象可能先 ASingleton 一步调用初始化构造函数, 结果 ASingleton::getInstance() 返回的就是一个未初始化的内存区域, 程序还没跑就直接崩掉。恩,这只是理论分析的结果,下面给出一个简单的例子说明一下问题所在吧!
    实例:ASingleton、BSingleton两个单例类,其中 ASingleton 的构造函数中使用到 BSingleton 的单例对象。
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() {}
};

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() {}
};

ASingleton ASingleton::m_data;
BSingleton BSingleton::m_data;

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

BSingleton::BSingleton()
{
    cout<<"BSingleton constructor!"<<endl;
}
         在这个测试例子中,我们将上述代码放在一个 main.cpp 文件中,其中 main 函数为空。 int main()
{
    return 0;
}
        运行测试结果是:ASingleton constructor!
BSingleton do_something!
BSingleton constructor!

奇怪了,为什么 BSingleton 的构造函数居然是在成员函数 do_something 之后调用的?
下面进行分析:    首先我们看到这个测试用例中,由于只有一个源文件,那么按从上到下的顺序进行编译运行。注意到:ASingleton ASingleton::m_data;
BSingleton BSingleton::m_data;
       这两个定义式,那么就会依次调用 ASingleton 的构造函数 和 BSingleton 的构造函数进行初始化。    一步一步来,首先是 ASingleto 的 m_data。    那么程序就会进入 ASingleton 的构造函数中执行,即:ASingleton::ASingleton()
{
    cout<<"ASingleton constructor!"<<endl;
    BSingleton::getInstance()->do_something();
}
        首先执行 cout,然后接着要获取 BSingleton 的单例,虽然说 BSingleton 的定义尚未执行,即  BSingleton BSingleton::m_data; 语句尚未执行到,但是 BSingleton 类中存在着其声明,那么还是可以调用到其 do_something 方法的。
    ASingleton 的构造函数执行完毕,那么 ASingleton ASingleton::m_data;  语句也就执行结束了,即 ASingleton 单例对象 m_data 也就初始化完成了。
    接下来执行 BSingleton BSingleton::m_data; 语句,那么也就是执行 BSingleton 的构造函数了。    所以就有了最终结果的输出了。
那么到此,我们或许会说:既然 ASingleton 的构造函数中要用到 BSingleton 单例对象,那么就先初始化 BSingleton 的单例对象咯,是的,我们可以调换一下顺序:
//ASingleton ASingleton::m_data;
//BSingleton BSingleton::m_data;
//修改成:
BSingleton BSingleton::m_data;
ASingleton ASingleton::m_data;

  再运行一下,会发现输出的结果就正常了。ASingleton constructor!
BSingleton constructor!
BSingleton do_something!
       问题解决了,那么我们通过这个问题实例,我们对于 静态成员变量 初始化顺序没有保障 有了更深刻的理解了。     在这个简单的例子中,我们通过调换代码位置可以保障 静态成员变量 的初始化顺序。但是在实际的编码中是不可能的,class 文件声明在头文件(.h)中,class 的定义在源文件(.cpp)中。而类静态成员变量声明是在 .h 文件中,定义在 .cpp 文件中,那么其初始化顺序就完全依靠编译器的心情了。所以这也就是 类静态成员变量 实现单例模式的致命缺点。     当然,假如不出现这种:在某单例的构造函数中使用到另一个单例对象  的使用情况,那么还是可以接受使用的。
    三、懒汉模式:单例实例只在第一次被使用时进行初始化:
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 分配内存并初始化. 嗯, 看上去所有的问题都解决了, 初始化顺序有保证, 多态也没问题.
    但是只是看似没有问题而已,其实其中存在着两个问题:
①线程不安全:我们注意到在 static Singleton* getInstance() 方法中,是通过 if 语句判断 静态实例变量 是否被初始化来觉得是否进行初始化,那么在多线程中就有可能出现多次初始化的问题。比方说,有两个多线程同时进入到这个方法中,同时执行 if 语句的判断,那么就会出现两次两次初始化静态实例变量的情况。
②析构函数没有被执行: 程序退出时, 析构函数没被执行. 这在某些设计不可靠的系统上会导致资源泄漏, 比如文件句柄, socket 连接, 内存等等. 幸好 Linux / Windows 2000/XP 等常用系统都能在程序退出时自动释放占用的系统资源. 不过这仍然可能是个隐患。   对于这个问题, 比较土的解决方法是, 给每个 Singleton 类添加一个 destructor() 方法:
virtual bool destructor()
{
    // ... release resource
    if (nullptr != m_data)
    {
        delete m_data;
        m_data = nullptr;
    }  
}
  
    然后在程序退出时确保调用了每个 Singleton 类的 destructor() 方法, 这么做虽然可靠, 但却很是繁琐.
  
    四、懒汉模式改进版:使用局部静态变量
class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton theSingleton;
        return theSingleton;
    }
    /* more (non-static) functions here */
 
private:
    Singleton();                            // ctor hidden
    Singleton(Singleton const&);            // copy ctor hidden
    Singleton& operator=(Singleton const&); // assign op. hidden
    ~Singleton();                           // dtor hidden
};

但是这种方式也存在着很多的问题: ①任意两个单例类的构造函数不能相互引用对方的实例,否则会导致程序崩溃。如: ASingleton& ASingleton::getInstance() {
    const BSingleton& b = BSingleton::getInstance();
    static ASingleton theSingleton;
    return theSingleton;
}
 
BSingleton& BSingleton::getInstance() {
    const ASingleton & b = ASingleton::getInstance();
    static BSingleton theSingleton;
    return theSingleton;
}

②多个 Singleton 实例相互引用的情况下, 需要谨慎处理析构函数. 如: 初始化顺序为 ASingleton »BSingleton » CSingleton 的三个 Singleton 类, 其中 ASingleton BSingleton 的析构函数调用了CSingleton 实例的成员函数, 程序退出时, CSingleton 的析构函数 将首先被调用, 导致实例无效, 那么后续 ASingleton BSingleton 的析构都将失败, 导致程序异常退出.
在局部作用域下的静态变量在编译时,编译器会创建一个附加变量标识静态变量是否被初始化,会被编译器变成像下面这样(伪代码): static Singleton &Instance()
{
  static bool constructed = false;
  static uninitialized Singleton instance_;
  if (!constructed) {
    constructed = true;
    new(&s) Singleton; //construct it
  }
  return instance_;
}

    那么,在多线程的应用场合下必须小心使用. 如果唯一实例尚未创建时, 有两个线程同时调用创建方法, 且它们均没有检测到唯一实例的存在, 便会同时各自创建一个实例, 这样就有两个实例被构造出来, 从而违反了单例模式中实例唯一的原则. 解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁 (虽然这样会降低效率).
加锁如下: static Singleton &getInstance()
{
    Lock();
    //锁自己实现 static
    Singleton instance_;
    UnLock();
 
    return instance_;
}
    但这样每次调用instance()都要加锁解锁,代价略大。
    五、终极方案     在前面的讨论中,单例类中的静态对象无论是作为静态局部对象还是作为类静态全局变量都有问题,那么有什么更好的解决方案呢?     boost 的实现方式是:单例对象作为静态局部变量,然后增加一个辅助类,并声明一个该辅助类的类静态成员变量,在该辅助类的构造函数中,初始化单例对象。 实现如下: 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;

   在前面的方案中:饿汉模式中,使用到了类静态成员变量,但是遇到了初始化顺序的问题; 懒汉模式中,使用到了静态局部变量,但是存在着线程安全等问题。    那么在这个终极方案中可以说综合了以上两种方案。即采用到了类静态成员变量,也采用到了静态局部变量。
    注意到其中的辅助结构体 Object_Creator (可以称之为 proxy-class)所声明的类静态成员变量,初始化该静态成员变量时,其中的构造函数 调用了单例类的 getInstance 方法。这样就会调用到 Singleton::getInstance() 方法初始化单例对象,那么自然 Singleton 的构造函数也就执行了。
    我们可以在Singleton 和 Object_Creator 的构造函数中添加一些输出信息:class Singleton
{
public:
    static Singleton* getInstance()
    {
        static Singleton instance;
        return &instance;
    }

protected:
    struct Object_Creator
    {
        Object_Creator()
        {
            cout<<"Object_Creator constructor"<<endl;
            Singleton::getInstance();
        }
    };
    static Object_Creator _object_creator;

    Singleton() {cout<<"Singleton constructor"<<endl;}
    ~Singleton() {}
};
Singleton::Object_Creator Singleton::_object_creator;

运行我们会看到(在 main 函数中还未使用到该单例):Object_Creator constructor
Singleton constructor
说明,此时在main函数之前就初始化了单例对象。
对于前面的ASingleton 和 BSingleton 的例子,改进如下: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;
}

    这样程序就避免了 ASingleton 和 BSingleton 单例对象的初始化顺序问题,使得输出结果就始终是:ASingleton constructor!
BSingleton constructor!
BSingleton do_something!

最后,展示一下添加了模板的实现:template <typename T>
class Singleton
{
    struct object_creator
    {
        object_creator()
        {
            Singleton<T>::instance();
        }
        inline void do_nothing() const {}
    };

    static object_creator create_object;

public:
    typedef T object_type;
    static T& instance()
    {
        static T obj;
        //这个do_nothing是确保create_object构造函数被调用
        //这跟模板的编译有关
        create_object.do_nothing();
        return obj;
    }

};
template <typename T> typename Singleton<T>::object_creator Singleton<T>::create_object;

class QMManager
{
protected:
    QMManager() {}
    ~QMManager() {}
    friend class Singleton<QMManager>;
public:
    void do_something() {};
};

int main()
{
    Singleton<QMManager>::instance().do_something();
    return 0;
}

   boost 通过添加一个类似 proxy-class 的方式,实现了单例模式,但是显然增加了复杂性,在实际应用中应该根据实际情况采用适当的实现方案。