如何在 C++ 中实现一个单例类模板

 更新时间:2020年10月28日 10:00:08   作者:始终  
这篇文章主要介绍了如何在 C++ 中实现一个单例类模板,帮助大家更好的理解和学习c++编程,感兴趣的朋友可以了解下

单例模式是最简单的设计模式之一。在实际工程中,如果一个类的对象重复持有资源的成本很高,且对外接口是线程安全的,我们往往倾向于将其以单例模式管理。

此篇我们在 C++ 中实现正确的单例模式。

选型

在 C++ 中,单例模式有两种方案可选。

  • 一是实现一个没有可用的公开构造函数的基类,并提供 GetInstance 之类的静态接口,以便访问子类唯一的对象。由于子类构造必须调用基类构造,但基类无公开构造函数可用,这使得子类对象只能由基类及基类的友元来构造,从而在机制上保证单例。
  • 二是实现一个类模板,其模板参数是希望由单例管理的类的名字,并提供 GetInstance 之类的静态接口。这种做法的好处是希望被单例管理的类,可以自由编写,而无需继承基类;并且在需要的时候,可以随时脱去单例外衣。

此篇选择实现一个单例类模板,其形如:

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.out
0x7fbc6f405a10
0x7fbc6f405a10
0x7fbc6f405a10
0x7fbc6f405a10
0x7fbc6f405a10
0x7fbc6f405a10
0x7fbc6f405a10
0x7fbc6f405a10
0x7fbc6f405a10
0x7fbc6f405a10
0x7fbc6f405a10

Bonus:需要注意的是,所有的 std::once_flag 内部共享了同一对互斥量和条件变量。因此当存在很多 std::call_once 的时候,性能会有所下降。这一点可能需要注意一下。不过,如果存在很多 std::call_once,大概也说明程序设计不合理吧……

Bonus:注意我们这里没有释放 p 指向的对象。这是因为 C++ 程序对静态变量的析构顺序是不确定的。如果静态变量之间有相互依赖,析构被依赖的对象可能会导致段错误。因此干脆就不释放了,这是所谓的 LeakySingleton。当然,如果你的工程当中有实现一个通用的 ExitManager,是有可能正确析构的。但考虑到还可能大量使用第三方库,而第三方库不可能使用你实现的 ExitManager,于是管理所有静态变量的析构又变得不可能,于是干脆就不管它了。

如此如此,这般这般

如果你仔细读了这篇文章,你可能会忽然意识到刚才看到了这句话:「在 C++11 之后,标准保证函数静态成员的初始化是线程安全的;对其读写则不保证线程安全。」

既然如此,我们为啥还要费劲使用 std::once_flagstd::call_once 呢?直接利用 static hack 出一个单例类模板不就好了吗?

template <typename T>
struct Singleton {
 static T* get() {
 static T ins;
 return &ins;
 }
 T* operator->() const {
 return get();
 }
};

以上就是如何在 C++ 中实现一个单例类模板的详细内容,更多关于c++ 单例类模板的资料请关注脚本之家其它相关文章!

相关文章

  • Sersync+Rsync实现触发式文件同步实战过程

    Sersync+Rsync实现触发式文件同步实战过程

    sersync是使用c++编写,而且对linux系统文 件系统产生的临时文件和重复的文件操作进行过滤。下面通过本文给大家分享Sersync+Rsync实现触发式文件同步实战过程,需要的朋友参考下吧
    2017-09-09
  • 推箱子游戏C语言实现代码

    推箱子游戏C语言实现代码

    这篇文章主要为大家详细介绍了推箱子游戏C语言实现代码,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-12-12
  • Android App仿微信界面切换时Tab图标变色效果的制作方法

    Android App仿微信界面切换时Tab图标变色效果的制作方法

    这篇文章主要介绍了Android App仿微信界面切换时Tab图标变色效果的制作方法,重点讲解了图标的绘制技巧,需要的朋友可以参考下
    2016-04-04
  • 用C语言判断字符是否为空白字符或特殊字符的方法

    用C语言判断字符是否为空白字符或特殊字符的方法

    这篇文章主要介绍了用C语言判断字符是否为空白字符或特殊字符的方法,分别为isspace()函数的使用和ispunct()函数的使用,需要的朋友可以参考下
    2015-08-08
  • 浅谈C++中char型变量的地址输出

    浅谈C++中char型变量的地址输出

    下面小编就为大家带来一篇浅谈C++中char 型变量的地址输出。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-09-09
  • C++ Eigen库计算矩阵特征值及特征向量

    C++ Eigen库计算矩阵特征值及特征向量

    这篇文章主要为大家详细介绍了C++ Eigen库计算矩阵特征值及特征向量,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-06-06
  • 使用C++递归求解跳台阶问题

    使用C++递归求解跳台阶问题

    这篇文章主要介绍了使用C++求解跳台阶问题的方法,通过递归算法来解决,不算难,文中给出了计算思路,需要的朋友可以参考下
    2016-02-02
  • C++求所有顶点之间的最短路径(用Floyd算法)

    C++求所有顶点之间的最短路径(用Floyd算法)

    这篇文章主要为大家详细介绍了C++求所有顶点之间的最短路径,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-04-04
  • 解析C++中构造函数的默认参数和构造函数的重载

    解析C++中构造函数的默认参数和构造函数的重载

    这篇文章主要介绍了解析C++中构造函数的默认参数和构造函数的重载,是C++入门学习中的基础知识,需要的朋友可以参考下
    2015-09-09
  • 详解C++的反调试技术与绕过手法

    详解C++的反调试技术与绕过手法

    反调试技术,恶意代码会用它识别自身是否被调试,或者让调试器失效,给反病毒工程师们制造麻烦,拉长提取特征码的时间线,本章将具体总结常见的反调试基础的实现原理以及如何过掉这些反调试手段,从而让我们能够继续分析恶意代码
    2021-06-06

最新评论