C++ 单例模式的模板实现
C++ 单例模式的模板实现
单例模式是一种创建型的设计模式(creational design patterns),使用单例模式进行设计的类在程序中只拥有一个实例(single instance),这个类称为单例类,它会提供一个全局的访问入口(global access point),关于单例模式的讨论可以参考Singleton revisited;基于这两个特点,单例模式可以有以下几种实现:
注意这种实例是通过返回引用使用的。所以需这样使用:Favorite& fav = Favorite::GetInstance(); 否则 Favorite fav = Favorite::GetInstance(); 会执行instance到fav这个新变量的赋值操作!!!不是单个实例了。
Meyer's Singleton
Scott Meyers 在 Effective C++ 的 Item 4: Make sure that objects are initialized before they're used 里面提出了一种利用 C++ 的 static
关键字来实现的单例模式,这种实现非常简洁高效,它的特点是:
- 仅当程序第一次执行到
GetInstance
函数时,执行instance
对象的初始化; - 在 C++ 11 之后,被
static
修饰的变量可以保证是线程安全的;
template<typename T> class Singleton { public: static T& GetInstance() { static T instance; return instance; } Singleton(T&&) = delete; Singleton(const T&) = delete; void operator= (const T&) = delete; protected: Singleton() = default; virtual ~Singleton() = default; };
通过禁用单例类的 copy constructor,move constructor 和 operator= 可以防止类的唯一实例被拷贝或移动;不暴露单例类的 constructor 和 destructor 可以保证单例类不会通过其他途径被实例化,同时将两者定义为 protected 可以让其被子类继承并使用。
Lazy Singleton
Lazy Singleton 是一种比较传统的实现方法,通过其名字可以看出来它也具有 lazy-evaluation 的特点,但在实现的时候需要考虑线程安全的问题:
template<typename T, bool is_thread_safe = true> class LazySingleton { private: static unique_ptr<T> t_; static mutex mtx_; public: static T& GetInstance() { if (is_thread_safe == false) { if (t_ == nullptr) t_ = unique_ptr<T>(new T); return *t_; } if (t_ == nullptr) { unique_lock<mutex> unique_locker(mtx_); if (t_ == nullptr) t_ = unique_ptr<T>(new T); return *t_; } } LazySingleton(T&&) = delete; LazySingleton(const T&) = delete; void operator= (const T&) = delete; protected: LazySingleton() = default; virtual ~LazySingleton() = default; }; template<typename T, bool is_thread_safe> unique_ptr<T> LazySingleton<T, is_thread_safe>::t_; template<typename T, bool is_thread_safe> mutex LazySingleton<T, is_thread_safe>::mtx_;
我们通过模板参数 is_thread_safe
来控制这个类是否是线程安全的,因为在某些场景下我们会希望每个线程拥有一个实例:
- 当
is_thread_safe == false
,即非线程安全时,我们在GetInstance
函数中直接判断,初始化并返回单例对象;这里使用了unique_ptr
防止线程销毁时发生内存泄漏,也可以在析构函数中销毁指针; - 当
is_thread_safe == true
时,我们通过 double-checked locking 来进行检查并加锁,防止单例类在每个线程上都被实例化。
Eager Singleton
和 Lazy Singleton 相反,Eager Singleton 利用 static member variable 的特性,在程序进入 main 函数之前进行初始化,这样就绕开了线程安全的问题:
template<typename T> class EagerSingleton { private: static T* t_; public: static T& GetInstance() { return *t_; } EagerSingleton(T&&) = delete; EagerSingleton(const T&) = delete; void operator= (const T&) = delete; protected: EagerSingleton() = default; virtual ~EagerSingleton() = default; }; template<typename T> T* EagerSingleton<T>::t_ = new (std::nothrow) T;
但是它也有两个问题:
- 即使单例对象不被使用,单例类对象也会进行初始化;
- static initialization order fiasco,即 t_ 对象和
GetInstance
函数的初始化先后顺序是不固定的;
Testing
将上面实现的四种 Singleton 分别继承下来作为 functor 传入线程对象进行测试:
class Foo : public Singleton<Foo> { public: void operator() () { cout << &GetInstance() << endl; } }; class LazyFoo : public LazySingleton<LazyFoo, false> { public: void operator() () { cout << &GetInstance() << endl; } }; class ThreadSafeLazyFoo : public LazySingleton<ThreadSafeLazyFoo> { public: void operator() () { cout << &GetInstance() << endl; } }; class EagerFoo : public EagerSingleton<EagerFoo> { public: void operator() () { cout << &GetInstance() << endl; } }; void SingletonTest() { thread t1((Foo())); thread t2((Foo())); t1.join(); t2.join(); this_thread::sleep_for(chrono::milliseconds(100)); t1 = thread((LazyFoo())); t2 = thread((LazyFoo())); t1.join(); t2.join(); this_thread::sleep_for(chrono::milliseconds(100)); t1 = thread((ThreadSafeLazyFoo())); t2 = thread((ThreadSafeLazyFoo())); t1.join(); t2.join(); this_thread::sleep_for(chrono::milliseconds(100)); t1 = thread((EagerFoo())); t2 = thread((EagerFoo())); t1.join(); t2.join(); }
输出结果为:
0x60d110 0x60d110 0x7f92380008c0 0x7f92300008c0 0x7f92300008e0 0x7f92300008e0 0x1132010 0x1132010
可以看到只有第二组非线程安全的 LazySingleton
在两个线程中输出的实例地址是不同的,其它的 Singleton 均是线程安全的。
注意thread调用,传递进去的是个仿函数。即 Foo()是调用构造函数。Foo()()对对象执行()调用才是调用的()函数。
参考:
C++中如何区分构造函数与重载operator()得到的仿函数
首先是定义形式:
-
构造函数无返回值,而operator是可以有返回值的;
-
定义时,构造函数需要类名,而重载operator()则不用;
其次是调用形式:
构造函数是声明对象,而仿函数则需要声明好的对象进行调用。
functor是仿函数,function是函数还是std::function?
如果是函数的话,个人感觉不会替代。因为毕竟C++是C的超集,而C中很多用到函数指针的地方,functor可能并不那么好用。
如果是std::function的话,那感觉也不会替代,因为std::function配合std::bind可以处理函数及仿函数。bind()接受一个函数(或者函数对象,或者任何你可以通过”(…)”符号调用的事物),生成一个其有某一个或多个函数参数被“绑定”或重新组织的函数对象。而functor好像不能以std::function作为参数传入。
话说仿函数即函数对象书写比较费事,所以C++ 11增加了lambda表达式,即匿名函数。函数对象即仿函数多用于C++ STL中,而在微软新加入的并行库PPL中,task等并不接受仿函数,多用lambda表达式。更容易。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
2019-03-11 纠错式教学法对比鼓励式教学法 -----Lily、贝乐、英孚,乐加乐、剑桥国际、优学汇、北外青少
2014-03-11 c++ 静态变量报错 undefined reference to static members