现代C++ 实现单例模式

传统写法有什么问题

如果你了解过单例模式,双重检查锁定模式(Double-Checked Locking Pattern,后文简称DCLP)的写法你一定不会陌生,甚至你或许认为它是最正确的代码。

cpp 复制代码
class Singleton {
public:
	//获取单例
	Singleton* GetInstance() {
		//双重检查
		if (p_instance==nullptr) {
			_mutex.lock();
			if (p_instance == nullptr)
				p_instance = new Singleton();
			_mutex.unlock();
		}
		return p_instance;
	}
private:
	//私有构造函数
	Singleton() = default;
public:
	~Singleton() = default;
private:
	//删除构造方法
	Singleton& operator=(const Singleton& obj) = delete;
	Singleton(const Singleton& obj) = delete;
	Singleton& operator=(const Singleton&& obj) = delete;
	Singleton(const Singleton&& obj) = delete;
private:
	static Singleton* p_instance;
	mutex _mutex;
};
Singleton* Singleton::p_instance = nullptr;//类外初始化

这几乎是最常见的单例模式,不过上面的代码却有大问题

问题就出现在这一句:

cpp 复制代码
				p_instance = new Singleton();

该问题在Effective C++的作者Scott Meyers关于单例的论文中被深刻讨论,如果你之前对单例模式缺乏了解,这篇论文你一定需要阅读:

C++ and the Perils of Double-Checked Lockinghttps://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf

其中提到,要执行该表达式 p_instance = new Singleton(); 需要做三件事:

  1. 开辟出用于存放单例的内存
  2. 在开辟的内存上构造Singleton对象
  3. 使p_instance指针指向开辟的内存

你期望,按照上述步骤去实例化单例,可是编译器没有任何约束去按你期望的顺序执行!

在特殊情况下,编译器有时会调换步骤二和步骤三,就像如下代码:

cpp 复制代码
	Singleton* GetInstance() {
		//双重检查
		if (p_instance==nullptr) {
			_mutex.lock();
			if (p_instance == nullptr) {
				p_instance = static_cast<Singleton*>	 //步骤三
					(operator new(sizeof(Singleton)));//步骤一
				new(p_instance) Singleton;			        //步骤二
			}
			_mutex.unlock();
		}
		return p_instance;
	}

此时执行顺序变为步骤一,步骤三,步骤二

如果你还没有理解,设想有如下场景:

  • 线程A 进入GetInstance,执行完判断,上锁,执行步骤三和步骤一。此时p_instance已经是一个非空的指针,但Singleton单例还未在p_instance所指向的空间上初始化。
  • 线程B 进入GetInstance,判断p_instance为非空,随后把p_instance返回。调用处对p_instance解引用,直接G!访问未初始化的内存。

DCLP只会在步骤一,步骤二在步骤三之前执行才能奏效,但他们的执行顺序确是未定义行为!编译器在这一点上不受任何约束,完全可能导致意想不到的问题。

**所以用DCLP实现的单例模式,单例对象的初始化顺序不确定。**这种情况可能导致在一个线程中访问尚未初始化的单例对象,从而引发错误。 并不是线程安全,因为包含未定义行为。

现代C++实现单例模式

为了规避这个问题,可利用C++11引入 魔法静态变量 特性,主要描述了具有静态存储期或线程存储期的块作用域变量的初始化规则。

§6.7 [stmt.dcl] p4原文: If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.

即,如果在变量(具有静态存储期或线程存储期的块作用域的)初始化过程中同时并发地再次进入声明,那么并发执行必须等待初始化过程完成。
§6.7 [stmt.dcl] p4http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf

利用这项标准,你就可以很爽的写出比DCLP更好的代码!

代码样例:

cpp 复制代码
class Singleton {
public:
	//现代写法
	Singleton& GetInstance() {
		// Let's create a magic static
		static Singleton instance;//C++11
		return instance;//return refence
	}
private:
	//私有构造函数
	Singleton() = default;
public:
	~Singleton() = default;
private:
	//删除构造方法
	Singleton& operator=(const Singleton& obj) = delete;
	Singleton(const Singleton& obj) = delete;
	Singleton& operator=(const Singleton&& obj) = delete;
	Singleton(const Singleton&& obj) = delete;
};

现代写法使用使用局部静态变量来实现单例模式。具体而言,文章提出的Meyers' Singleton就是基于这一思想的解决方案。

如果本文帮助到你,希望能点赞收藏,欢迎留言讨论。

相关推荐
charlie1145141911 分钟前
通用GUI编程技术——Win32 原生编程实战(二十二)——GDI 位图操作:BitBlt、StretchBlt 与图像处理
c++·windows·学习·c·win32
ZPC82107 小时前
如何创建一个单例类 (Singleton)
开发语言·前端·人工智能
Darkwanderor7 小时前
什么数据量适合用什么算法
c++·算法
超绝振刀怪7 小时前
【C++多态】
开发语言·c++
workflower8 小时前
AI制造-推荐初始步骤
java·开发语言·人工智能·软件工程·制造·需求分析·软件需求
zc.ovo8 小时前
河北师范大学2026校赛题解(A,E,I)
c++·算法
魔都吴所谓8 小时前
【Python】从零构建:IP地理位置查询实战指南
开发语言·python·tcp/ip
环黄金线HHJX.8 小时前
【吧里BaLi社区】
开发语言·人工智能·qt·编辑器
学嵌入式的小杨同学9 小时前
STM32 进阶封神之路(三十九)FreeRTOS 临界区、挂起 / 删除、钩子函数、调度底层原理|从应用到内核深度解析
c++·stm32·单片机·嵌入式硬件·mcu·硬件架构·pcb