C++ 单例模式和可继承的单例基类模板

本文包括C++ 11的特性如智能指针, magic static,线程锁;本文的全部代码在 g++ 5.4.0 编译器下编译运行通过。

一、什么是单例

单例 Singleton 是设计模式的一种,其特点是只提供唯一一个类的实例,具有全局变量的特点,在任何位置都可以通过接口获取到那个唯一实例;
具体运用场景如:

  1. 设备管理器,系统中可能有多个设备,但是只有一个设备管理器,用于管理设备驱动;
  2. 数据池,用来缓存数据的数据结构,需要在一处写,多处读取或者多处写,多处读取;

二、C++单例的实现

2.1 基础要点

  • 全局只有一个实例:static 特性,同时禁止用户自己声明并定义实例(把构造函数设为 private)
  • 线程安全
  • 禁止赋值和拷贝
  • 用户通过接口获取实例:使用 static 类成员函数

2.2 C++ 实现单例的几种方式

2.2.1 有缺陷的懒汉式

懒汉式(Lazy-Initialization)的方法是直到使用时才实例化对象,也就说直到调用get_instance() 方法的时候才 new 一个单例的对象, 如果不被调用就不会占用内存。

#include 
// version1:
// with problems below:
// 1. thread is not safe
// 2. memory leak

class Singleton{
private:
    Singleton(){
        std::cout<<"constructor called!"<

运行的结果是constructor called!

可以看到,获取了两次类的实例,却只有一次类的构造函数被调用,表明只生成了唯一实例,这是个最基础版本的单例实现,他有哪些问题呢?

  1. 线程安全的问题,当多线程获取单例时有可能引发竞态条件:第一个线程在if中判断 m_instance_ptr是空的,于是开始实例化单例;同时第2个线程也尝试获取单例,这个时候判断m_instance_ptr还是空的,于是也开始实例化单例;这样就会实例化出两个对象,这就是线程安全问题的由来; 解决办法:加锁
  2. 内存泄漏. 注意到类中只负责new出对象,却没有负责delete对象,因此只有构造函数被调用,析构函数却没有被调用;因此会导致内存泄漏。解决办法: 使用共享指针;

因此,这里提供一个改进的,线程安全的、使用智能指针的实现;

2.2.2 线程安全、内存安全的懒汉式单例 (智能指针,锁)

#include 
#include  // shared_ptr
#include   // mutex

// version 2:
// with problems below fixed:
// 1. thread is safe now
// 2. memory doesn't leak

class Singleton{
public:
    typedef std::shared_ptr Ptr;
    ~Singleton(){
        std::cout<<"destructor called!"< lk(m_mutex);
            if(m_instance_ptr == nullptr){
              m_instance_ptr = std::shared_ptr(new Singleton);
            }
        }
        return m_instance_ptr;
    }


private:
    Singleton(){
        std::cout<<"constructor called!"<

运行结果如下,发现确实只构造了一次实例,并且发生了析构。

constructor called!

destructor called!

shared_ptr和mutex都是C++11的标准,以上这种方法的优点是

  • 基于 shared_ptr, 用了C++比较倡导的 RAII思想,用对象管理资源,当 shared_ptr 析构的时候,new 出来的对象也会被 delete掉。以此避免内存泄漏。
  • 加了锁,使用互斥量来达到线程安全。这里使用了两个 if判断语句的技术称为双检锁;好处是,只有判断指针为空的时候才加锁,避免每次调用 get_instance的方法都加锁,锁的开销毕竟还是有点大的。

不足之处在于: 使用智能指针会要求用户也得使用智能指针,非必要不应该提出这种约束; 使用锁也有开销; 同时代码量也增多了,实现上我们希望越简单越好。

还有更加严重的问题,在某些平台(与编译器和指令集架构有关),双检锁会失效!

因此这里还有第三种的基于 Magic Staic的方法达到线程安全

2.2.3 最推荐的懒汉式单例(magic static )——局部静态变量--推荐

#include 

class Singleton
{
public:
    ~Singleton(){
        std::cout<<"destructor called!"<

运行结果constructor called!

destructor called!

用到的特性是在C++11标准中的Magic Static特性:

这样保证了并发线程在获取静态局部变量的时候一定是初始化过的,所以具有线程安全性。 C++静态变量的生存期 是从声明到程序结束,这也是一种懒汉式。

这是最推荐的一种单例实现方式:

1、通过局部静态变量的特性保证了线程安全 (C++11, GCC > 4.3, VS2015支持该特性);

2、不需要使用共享指针,代码简洁;

3、注意在使用的时候需要声明单例的引用 Single& 才能获取对象。

2.3 单例的模板

2.3.1 CRTP 奇异递归模板模式实现--推荐

// brief: a singleton base class offering an easy way to create singleton
#include 

template
class Singleton{
public:
    static T& get_instance(){
        static T instance;
        return instance;
    }
    virtual ~Singleton(){
        std::cout<<"destructor called!"<{
   // !!!! attention!!!
   // needs to be friend in order to
   // access the private constructor/destructor
   friend class Singleton;
public:
   DerivedSingle(const DerivedSingle&)=delete;
   DerivedSingle& operator =(const DerivedSingle&)= delete;
private:
   DerivedSingle()=default;
};

int main(int argc, char* argv[]){
    DerivedSingle& instance1 = DerivedSingle::get_instance();
    DerivedSingle& instance2 = DerivedSingle::get_instance();
    return 0;
}

以上实现一个单例的模板基类,使用方法如例子所示意,子类需要将自己作为模板参数T 传递给 Singleton 模板; 同时需要将基类声明为友元,这样才能调用子类的私有构造函数。

基类模板的实现要点是:

1、基类构造函数需要是 protected,这样子类才能继承;

2、使用了奇异递归模板模式CRTP(Curiously recurring template pattern

3、)get instance 方法和 2.2.3 的static local方法一个原理。

4、在这里基类的析构函数可以不需要 virtual ,因为子类在应用中只会用 Derived 类型,保证了析构时和构造时的类型一致

2.3.2 不需要在子类声明友元的实现方法

使用一个代理类 token,子类构造函数需要传递token类才能构造,但是把 token保护其起来, 然后子类的构造函数就可以是公有的了,这个子类只有 DerivedSingle(token)的这样的构造函数,这样用户就无法自己定义一个类的实例了,起到控制其唯一性的作用。

// brief: a singleton base class offering an easy way to create singleton
#include 

template
class Singleton{
public:
    static T& get_instance() noexcept(std::is_nothrow_constructible::value){
        static T instance{token()};
        return instance;
    }
    virtual ~Singleton() =default;
    Singleton(const Singleton&)=delete;
    Singleton& operator =(const Singleton&)=delete;
protected:
    struct token{}; // helper class
    Singleton() noexcept=default;
};


/********************************************/
// Example:
// constructor should be public because protected `token` control the access
class DerivedSingle:public Singleton{
public:
   DerivedSingle(token){
       std::cout<<"destructor called!"<

2.3.3 函数模板返回引用

在 2.2.4 中提供了一种类型的全局变量的方法,可以把一个一般的类,通过这种方式提供一个类似单例的
全局性效果(但是不能阻止用户自己声明定义这样的类的对象);在这里我们把这个方法变成一个 template 模板函数,然后就可以得到任何一个类的全局变量。

#include 

class A
{
public:
    A() {
        std::cout<<"constructor" <
T& get_global(){
    static T instance;
    return instance;
}

int main(int argc, char *argv[])
{
    A& instance_1 = get_global();
    A& instance_2 = get_global();
    return 0;
}

可以看到这种方式确实非常简洁,同时类仍然具有一般类的特点而不受限制,当然也因此失去了单例那么强的约束(禁止赋值、构造和拷贝构造)。
这里把函数命名为 get_global() 是为了强调,这里可以通过这种方式获取得到单例最重要的全局变量特性;但是并不是单例的模式。

何时应该使用或者不使用单例

1、你需要系统中只有唯一一个实例存在的类的全局变量的时候才使用单例

2、如果使用单例,越小越好,越简单越好,线程安全,内存不泄露

2.4 C++可继承的单例基类模板

// author: sunchaothu
// brief: a singleton base class offering an easy way to create singleton
#include 

template
class Singleton{
public:
    static T& get_instance() noexcept(std::is_nothrow_constructible::value){
        static T instance;
        return instance;
    }
    virtual ~Singleton() noexcept{
        std::cout<<"destructor called!"<{
   // !!!! attention!!!
   // needs to be friend in order to
   // access the private constructor/destructor
   friend class Singleton;
public:
   DerivedSingle(const DerivedSingle&)=delete;
   DerivedSingle& operator =(const DerivedSingle&)= delete;
private:
   DerivedSingle()=default;
};

int main(int argc, char* argv[]){
    DerivedSingle& instance1 = DerivedSingle::get_instance();
    DerivedSingle& instance2 = DerivedSingle::get_instance();
    return 0;
}

关键处

  • 子类需要把自己作为模板参数,如 class DerivedSingle:public Singleton
    这里用到的是CRTP(Curiously recurring template pattern) 奇异循环模板模式
  • 在子类中需要把基类 Singleton 声明为友元;这样才能访问私有构造函数
  • 这个使用了局部静态变量保证了线程安全,成为 Magic Static。

使用限制

继承了这个类的代码不可以作为基类再被继承。

class A
{
public:
	A() = default;
	A(int a) : _a(a){}
	//C++11
	// 禁止编译器生成默认的拷贝构造函数以及赋值运算符重载
	A(const A&) = delete;
	A& operator=(const A&) = delete;
private:
	int _a;
	//C++98,设置成private就可以了
	A(const A&) = private;
	A& operator=(const A&) = private;
};

你可能感兴趣的:(C++,单例模式,c++,开发语言)