C++设计模式之单例模式

单例模式

单例模式是面向对象编程中最常用的一种模式,写法也有很多种,比如 懒汉式和饿汉式 。懒汉式比较简单,就是整一个全局的对象,也没有线程安全的问题,但使用全局变量,封装性差了一点点。饿汉式比较常用一点,核心思想就是在实例化时创建静态对象,但是会有线程安全的问题。本章针对饿汉式的单例模式进行详细说明。


普通单例模式

先写一个加锁的单例模式,代码如下

// 单例模式。加锁,双重校验
template<typename T>
class Singleton {
public:
    static T* Instance() {
        static std::mutex mtx;
        if (instance_ == nullptr) {
            std::lock_guard<std::mutex> lock(mtx);
            // 第二次判断是为防止其他线程已经创建实例,不能再次创建
            if (instance_ == nullptr)
                instance_ = std::make_shared<T>();
        }
        return instance_.get();
    }
private:
    static std::shared_ptr<T> instance_;
};

// 静态成员变量必须要做一个初始化
template<typename T>
std::shared_ptr<T> Singleton<T>::instance_ = nullptr;

这里之所以要对 instance_ 做两次判断,是为防止重复创建对象,即在第二次判断前(8~11行代码之间),其他线程已经创建好了 instance_ 对象。

在 C++11 中规定静态局部变量的初始化是线程安全的,可以利用这一特性,写一个更简单的单例模式,代码如下

// 单例模式。静态局部变量,C++11保证静态局部变量的初始化是线程安全的
template<typename T>
class Singleton {
public:
    static T* Instance() {
        static std::shared_ptr<T> instance = std::make_shared<T>();
        return instance.get();
    }
};

可见,确实比第一种方法简单很多。


泛型单例模式

上面两种单例模式虽然也使用了模板,但是都要求单例类必须是无参构造,如果构造函数必须要输入参数,就无法使用了,不过只需要使用 C++11 中的模板参数和 std::forward 方法就可以解决。代码分为加锁版本和不加锁版本:

#ifdef WITH_MUTEX

// 单例模式。加锁,双重校验
template<typename T>
class Singleton {
public:
    template<typename ...Args>
    static T* Instance(Args&& ...args) {
        static std::mutex mtx;
        if (instance_ == nullptr) {
            std::lock_guard<std::mutex> lock(mtx);
            // 第二次判断是为防止其他线程已经创建实例,不能再次创建
            if (instance_ == nullptr)
                instance_ = std::make_shared<T>(std::forward<Args>(args)...);
        }
        return instance_.get();
    }
private:
    static std::shared_ptr<T> instance_;
};

// 静态成员变量必须要做一个初始化
template<typename T>
std::shared_ptr<T> Singleton<T>::instance_ = nullptr;

#else

// 单例模式。静态局部变量,C++11保证静态局部变量的初始化是线程安全的
// 使用C++11转移语义构造输入参数
template<typename T>
class Singleton {
public:
    template<typename ...Args>
    static T* Instance(Args&& ...args) {
        static std::shared_ptr<T> instance = std::make_shared<T>(std::forward<Args>(args)...);
        return instance.get();
    }
};

#endif

代码定义了两类单例模式:加锁和使用 C++11 局部变量,使用宏定义 WITH_MUTEX 分开,实际使用时,选择任意一种都可以。但有细微地方需要注意,会在下面的测试代码中进行说明。


测试代码

单例模式的测试代码如下:

#include <iostream>
#include <vector>
#include <assert.h>
#include "singleton.h"

class TestObj1 {
public:
    TestObj1() { std::cout << "TestObj1" << std::endl; }
};

class TestObj2 {
public:
    TestObj2(int n, float f) { std::cout << "TestObj2, n=" << n << ", f=" << f << std::endl; }
};

class TestObj3 {
public:
    TestObj3() { std::cout << "TestObj3, str=null" << std::endl; }
    TestObj3(std::string str) { std::cout << "TestObj3, str=" << str << std::endl; }
};

class TestObj4 {
public:
    TestObj4(std::vector<int> v) {
        std::cout << "TestObj4, vector=[";
        for (size_t i = 0; i < v.size(); ++i)
            std::cout << v[i] << " ";
        std::cout << "]" << std::endl;
    }
};

int main(int argc, char* argv[])
{
    TestObj1 *obj1 = Singleton<TestObj1>::Instance();
    assert(obj1 != nullptr);

    TestObj1 *other1 = Singleton<TestObj1>::Instance();
    assert(other1 == obj1);

    TestObj2 *obj2 = Singleton<TestObj2>::Instance(1, 1.2);
    assert(obj2 != nullptr);

    TestObj2 *other2 = Singleton<TestObj2>::Instance(2, 2.2);
    assert(other2 == obj2);

    // 不能直接用字符串常量作为参数,即
    // TestObj3 *obj3 = Singleton<TestObj3>::Instance("test");
    // 因为一旦常量参数变化,单例模板会特化一个新的类,会再次调用构造函数。
    // 同理,若构造函数有重载,则必须调用相同构造函数才能获取相同对象,
    // 不同的构造函数模板会特化成不同的类。
    std::string str1("test");
    TestObj3* obj3 = Singleton<TestObj3>::Instance(str1);
    assert(obj3 != nullptr);

    std::string str2 = "other";
    TestObj3 *other3 = Singleton<TestObj3>::Instance(str2);
    assert(other3 == obj3);

    TestObj3 *other3_1 = Singleton<TestObj3>::Instance();
#ifdef WITH_MUTEX
    assert(other3_1 == obj3);
#else
    assert(other3_1 != obj3);
#endif

    std::vector<int> v1 {1, 2, 3, 4, 5};
    TestObj4 *obj4 = Singleton<TestObj4>::Instance(v1);
    assert(obj4 != nullptr);

    std::vector<int> v2 {11, 12, 13, 14, 15};
    TestObj4 *other4 = Singleton<TestObj4>::Instance(v2);
    assert(other4 == obj4);

    return 0;
}

其他测试对象都比较好理解,但是对于 TestObj3 ,有两点需要注意的地方:

  • 不能直接用字符串常量作为参数,即 Singleton<TestObj3>::Instance("test") ,因为编译器会针对每一个不同的常量参数将模板特化为不同的类,很显然这不符合使用单例的效果,而是需要传入 std::string 类型的参数。

  • 对于有重载构造函数的类,加锁与使用 C++11 静态变量是有区别的,参见代码 59~64 行,因为

    • 加锁的方式使用的是静态局部变量,模板对象不论调用哪一个构造函数都不会改变这个对象,返回的也是最开始创建的对象,与模板对象的构造函数无关;
    • C++11 静态变量的方式不是,针对不同的 Singleton<T>::Instance 参数,模板会被特化成不同的类,所以 不同的构造函数会创造不同的单例实例 ,即与模板对象的构造函数相关。

    这两种方式各有不同,需要针对实际应用灵活处理。


参考资料

  1. c++11设计模式 单例模式 线程安全 泛型单例
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值