时间:2021-05-19
单例模式是最简单的设计模式之一。在实际工程中,如果一个类的对象重复持有资源的成本很高,且对外接口是线程安全的,我们往往倾向于将其以单例模式管理。
此篇我们在 C++ 中实现正确的单例模式。
选型
在 C++ 中,单例模式有两种方案可选。
此篇选择实现一个单例类模板,其形如:
template <typename T>struct Singleton { static T* get(); T* operator->() const { return get(); }};这里重载成员访问运算符,是为了可以实现这样的简写 Singleton<T>()->func()。
显然,单例的实现核心在于静态成员函数 T* get()。
一个典型的错误实现
一个典型的错误实现,是使用所谓的双重检查(double check)。
#include <mutex>template <typename T>struct Singleton { static T* get() { static T* p{nullptr}; if (nullptr == p) { std::lock_guard<std::mutex> lock{mtx}; if (nullptr == p) { p = new T; } } return p; } T* operator->() const { return get(); } private: static std::mutex mtx;};template <typename T>std::mutex Singleton<T>::mtx;外层的检查,是为了避免锁住过大的区域,从而导致锁的竞争特别频繁;内层的检查,是为了确保只在别的线程没有提前抢占锁完成初始化工作而设计的。这种做法在 Java 下是正确的,但是在 C++ 下则没有保证。
另外,值得一提的是,这里 p 的初始化的线程安全性,是由 C++ 标准保证的。——在 C++11 之后,标准保证函数静态成员的初始化是线程安全的;对其读写则不保证线程安全。
使用标准库提供的设施
在单例的实现中,我们实际上是希望实现「执行且只执行一次」的语义。C++11 之后,标准库实际已经提供了这样的设施。其名为 std::once_flag 和 std::call_once。它们内部利用互斥量和条件变量组合,实现这样的语义。值得一提的是,如果执行过程中抛出异常,标准库的设施不认为这是一次「成功的执行」。于是其他线程可以继续抢占锁来执行函数。
我们利用标准库设施来实现这个类模板。
#include <mutex>template <typename T>struct Singleton { static T* get() { static T* p{nullptr}; std::call_once(flag, [&]() -> void { p = new T; }); return p; } T* operator->() const { return get(); } private: static std::once_flag flag;};template <typename T>std::once_flag Singleton<T>::flag;于是你可以写出类似这样的代码:
#include <mutex>#include <iostream>#include <future>#include <vector>#include "singleton.h"struct Foo { void address() const { std::lock_guard<std::mutex> lock{mtx}; std::cout << static_cast<void*>(const_cast<Foo*>(this)) << '\n'; } mutable std::mutex mtx;};int main() { Singleton<Foo>()->address(); std::vector<std::future<void>> futs; for (size_t i = 0; i != 10; ++i) { futs.emplace_back(std::async(&Foo::address, Singleton<Foo>::get())); } for (auto& fut : futs) { fut.get(); } return 0;}得到的输出类似这样:
$ ./a.out0x7fbc6f405a100x7fbc6f405a100x7fbc6f405a100x7fbc6f405a100x7fbc6f405a100x7fbc6f405a100x7fbc6f405a100x7fbc6f405a100x7fbc6f405a100x7fbc6f405a100x7fbc6f405a10Bonus:需要注意的是,所有的 std::once_flag 内部共享了同一对互斥量和条件变量。因此当存在很多 std::call_once 的时候,性能会有所下降。这一点可能需要注意一下。不过,如果存在很多 std::call_once,大概也说明程序设计不合理吧……
Bonus:注意我们这里没有释放 p 指向的对象。这是因为 C++ 程序对静态变量的析构顺序是不确定的。如果静态变量之间有相互依赖,析构被依赖的对象可能会导致段错误。因此干脆就不释放了,这是所谓的 LeakySingleton。当然,如果你的工程当中有实现一个通用的 ExitManager,是有可能正确析构的。但考虑到还可能大量使用第三方库,而第三方库不可能使用你实现的 ExitManager,于是管理所有静态变量的析构又变得不可能,于是干脆就不管它了。
如此如此,这般这般
如果你仔细读了这篇文章,你可能会忽然意识到刚才看到了这句话:「在 C++11 之后,标准保证函数静态成员的初始化是线程安全的;对其读写则不保证线程安全。」
既然如此,我们为啥还要费劲使用 std::once_flag 和 std::call_once 呢?直接利用 static hack 出一个单例类模板不就好了吗?
template <typename T>struct Singleton { static T* get() { static T ins; return &ins; } T* operator->() const { return get(); }};以上就是如何在 C++ 中实现一个单例类模板的详细内容,更多关于c++ 单例类模板的资料请关注其它相关文章!
声明:本页内容来源网络,仅供用户参考;我单位不保证亦不表示资料全面及准确无误,也不保证亦不表示这些资料为最新信息,如因任何原因,本网内容或者用户因倚赖本网内容造成任何损失或损害,我单位将不会负任何法律责任。如涉及版权问题,请提交至online#300.cn邮箱联系删除。
本文实例为大家分享了C++利用链表模板类实现一个队列的具体代码,供大家参考,具体内容如下设计思想:MyQueue.h中对模板类进行声明和实现。首先定义结点的结构
javascript中模板方法单例的实现方法模板方法单例模板方法的定义:父类中定义一组操作算法骨架,将一些实现步骤延伸到子类中,使得子类可以不改变父类的算法结构
本文实例为大家分享了C++使用模板实现单链表的具体代码,供大家参考,具体内容如下这一篇可以和上一篇点击打开链接模板实现单链表进行对比看类外实现和类内实现的区别代
C++单例模式的详解及实例1.什么叫单例模式?单例模式也称为单件模式、单子模式,可能是使用最广泛的设计模式。其意图是保证一个类仅有一个实例,并提供一个访问它的全
C++类的多继承在前面的例子中,派生类都只有一个基类,称为单继承。除此之外,C++也支持多继承,即一个派生类可以有两个或多个基类。多继承容易让代码逻辑复杂、思路