单例模式
单例模式是面向对象编程中最常用的一种模式,写法也有很多种,比如 懒汉式和饿汉式 。懒汉式比较简单,就是整一个全局的对象,也没有线程安全的问题,但使用全局变量,封装性差了一点点。饿汉式比较常用一点,核心思想就是在实例化时创建静态对象,但是会有线程安全的问题。本章针对饿汉式的单例模式进行详细说明。
普通单例模式
先写一个加锁的单例模式,代码如下
// 单例模式。加锁,双重校验
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
参数,模板会被特化成不同的类,所以 不同的构造函数会创造不同的单例实例 ,即与模板对象的构造函数相关。
这两种方式各有不同,需要针对实际应用灵活处理。