单例九品--第八品[可用的设计]

单例九品--第八品[可用的设计]

  • 上一品引入
  • 写在前边
  • 代码部分1
  • 代码部分2
  • 代码部分3
  • 实现方式评注与思考
  • 下一品设计的思考

上一品引入

第七品使用std::atomic静态函数实现了计数变量singletonCount的定义,解决了第六品引入的多线程安全的问题,但是从第五品开始全局对象都换成了指针的类型。因此一直存在着对象可能被修改的风险。第八品将来解决这个问题。

写在前边

  • 基本思路
    • 使用引用代替指针
    • 方法1:将原始指针封装到类内部,使用函数提供引用接口
    • 方法2:使用 placement new
  • 优点
    • 避免指针赋值而产生的问题
  • 缺点
    • 代码组织相对复杂,不易扩展

本篇章将使用两种方法解决前边几品出现的指针类型的全局对象被修改的问题。

代码部分1

三个文件: main.cpp, sing.cpp和sing.h

  • main.cpp
#include "sing.h"
static Sing::Init init;
static Sing& singletonInst = Sing::Instance();auto singletonInst2 = singletonInst.val;int main(int argc, char** argv)
{std::cout << "get value: " << singletonInst2 << '\n';std::cout << Sing::Instance().val << std::endl;
}
  • sing.cpp
#include "sing.h"
#include <memory>
#include <iostream>Sing* Sing::singletonPtr;Sing::Init::Init()
{auto& count = RefCount();auto ori = count.fetch_add(1);if (ori == 0){singletonPtr = new Sing();}
}Sing::Init::~Init()
{auto& count = RefCount();auto ori = count.fetch_sub(1);if (ori == 1){delete singletonPtr;singletonPtr = nullptr;}
}
  • sing.h
#pragma once
#include <iostream>
#include <atomic>class Sing
{
public:struct Init{Init();Init(const Init&) = delete;Init& operator= (const Init&) = delete;~Init();static auto& RefCount(){static std::atomic<unsigned> count{ 0 };return count;}};private:Sing(){std::cout << "Sing construct\n";val = 100;}~Sing(){std::cout << "Sing destroy\n";}Sing(const Sing&) = delete;Sing& operator= (const Sing&) = delete;public:int val;private:static Sing* singletonPtr;public:static Sing& Instance(){return *singletonPtr;}
};

在前边的实现中,都是定义一个指针类型的全局单例对象,然后通过静态函数instance完成单例的初始化。在这里实现的方式是,在sing类中定义一个私有sing*类型的全局对象,然后提供一个全局对象的public引用访问接口,一方面私有的全局对象能避免一些sing类之外的一切行为修改。另一方面, 共有的引用接口函数返回的是全局对象的引用,引用绑定以后,不会更换绑定对象,因此就避免了全局对象被修改的风险。这种实现方式是可以使用的单例设计模式了。

代码部分2

三个文件: sing.cpp main.cpp 和sing.h

  • sing.cpp
#include "sing.h"
#include <memory>
#include <iostream>alignas(Sing) char singBuf[sizeof(Sing)]; 
Sing& singletonInst = *(reinterpret_cast<Sing*>(singBuf));Sing::Init::Init()
{auto& count = RefCount();auto ori = count.fetch_add(1);if (ori == 0){Sing* ptr = reinterpret_cast<Sing*>(singBuf);new (ptr) Sing();  // placement new的方式, 与new的方式构建对象有区别}
}Sing::Init::~Init()
{auto& count = RefCount();auto ori = count.fetch_sub(1);if (ori == 1){Sing* ptr = reinterpret_cast<Sing*>(singBuf);ptr->~Sing();}
}
  • main.cpp
#include "sing.h"auto singletonInst2 = singletonInst.val;int main(int argc, char** argv)
{std::cout << "get value: " << singletonInst2 << '\n';std::cout << singletonInst.val << std::endl;return 0;
}
  • sing.h
#pragma once
#include <iostream>
#include <atomic>class Sing
{
public:struct Init{Init();Init(const Init&) = delete;Init& operator= (const Init&) = delete;~Init();static auto& RefCount(){static std::atomic<unsigned> count{ 0 };return count;}};private:Sing(){std::cout << "Sing construct\n";val = 100;}~Sing(){std::cout << "Sing destroy\n";}Sing(const Sing&) = delete;Sing& operator= (const Sing&) = delete;public:int val;
};static Sing::Init init;
extern Sing& singletonInst;

这个例子,使用reinterpret_cast强制类型转换,通过placement new的方式完成的单例构造。这个写法在这种情况下(因为reinterpret_cast强制转换在这种情况下是在编译期完成的)是可以正常运行,并且不会因为翻译单元链接顺序造成静态初始化灾难的问题。

翻译单元中的这两句是是编译期的行为,具体解释如下:

alignas(Sing) char singBuf[sizeof(Sing)];  
Sing& singletonInst = *(reinterpret_cast<Sing*>(singBuf));
  • alignas(Sing) char singBuf[sizeof(Sing)]; 这行代码是在编译期声明了一个名为 singBuf 的数组,其大小为 sizeof(Sing),并要求按照 Sing 类型的对齐方式进行对齐。这意味着编译器会在编译期为这个数组选择合适的内存布局和对齐方式。

  • Sing& singletonInst = (reinterpret_cast<Sing>(singBuf)); 这行代码在编译期进行了类型转换和引用绑定。reinterpret_cast 是一种类型转换操作符,它在编译期将 singBuf 的地址转换为 Sing* 类型的指针,然后通过解引用 * 操作符将其转换为 Sing& 类型的引用,最终将这个引用绑定到了 singletonInst 变量上。
    所以,这两行代码都是在编译期执行的,而不是在运行期执行的。它们的目的是在编译期进行一些内存布局和类型转换的操作,以便在运行期能够正确地使用 singletonInst 变量。

  • reinterpret_cast 强制转换什么情况是运行期的行为,什么时候是编译期的行为?
    答: 对于大多数情况来说,reinterpret_cast 操作符是在编译期完成的,但是在一些特殊情况下,比如使用了动态内存分配或者其它运行期的特性时,reinterpret_cast 的结果可能会在运行期才被确定。
    在上述例子中,reinterpret_cast 的结果是一个编译时已知的常量表达式,因为它是根据 singBuf 的类型进行计算的,而 singBuf 是一个静态数组,其大小在编译期已知。因此,在这个特定的情况下,reinterpret_cast 的结果是在编译期确定的,而不是在运行期。

所以这里即便是不加constinit关键字,使Sing& singletonInst = *(reinterpret_cast<Sing*>(singBuf));在编译期完成强制转换,也不会因为链接顺序出现静态初始化灾难的问题。但是对于其他特定情况,就不行了,所以这种写法不具备通用性。因此最好在sing.cpp中写为constinit Sing& singletonInst = *(reinterpret_cast<Sing*>(singBuf))

那如果当reinterpret_cast面临不能自动在编译期完成的时候的情况,可以不使用constinit关键字解决及静态编译器灾难吗?请看下一个例子

代码部分3

三个文件: sing.h sing.cpp main.cpp
与代码部分2的区别就是将Sing& singletonInst = *(reinterpret_cast<Sing*>(singBuf))放在sing.h中,这样在main.cpp中使用单例对象的时候就一定已经完成了初始化;

  • sing.h
#pragma once
#include <iostream>
#include <atomic>class Sing
{
public:struct Init{Init();Init(const Init&) = delete;Init& operator= (const Init&) = delete;~Init();static auto& RefCount(){static std::atomic<unsigned> count{ 0 };return count;}};private:Sing(){std::cout << "Sing construct\n";val = 100;}~Sing(){std::cout << "Sing destroy\n";}Sing(const Sing&) = delete;Sing& operator= (const Sing&) = delete;public:int val;
};static Sing::Init init;   // 完成Sing实例的构建,使用newplace new的方式在编译器已经分配好的那块地址上
extern char singBuf[sizeof(Sing)];
static Sing& singletonInst = *(reinterpret_cast<Sing*>(singBuf));
  • sing.cpp
#include "sing.h"
#include <memory>
#include <iostream>alignas(Sing) char singBuf[sizeof(Sing)];  // 这句话在编译期会完成内存分配和内存对齐,也就是说会为char类型的字符数组singBuf分配一定大小的内存,但是具体的内存块(地址)// 要在运行期决定。Sing::Init::Init()
{auto& count = RefCount();auto ori = count.fetch_add(1);if (ori == 0){Sing* ptr = reinterpret_cast<Sing*>(singBuf);new (ptr) Sing();  // placement new的方式, 与new的方式构建对象有区别//这种情况下,singBuf是一个字符数组,其内存地址由编译器分配,但是具体地址在编译时尚未确定。//可以使用 placement new 在这个数组的内存上构建对象,因为编译器会为 new (singBuf) Sing() 提供所需的地址信息}
}Sing::Init::~Init()
{auto& count = RefCount();auto ori = count.fetch_sub(1);if (ori == 1){Sing* ptr = reinterpret_cast<Sing*>(singBuf);ptr->~Sing();}
}
  • main.cpp
#include "sing.h"
auto singletonInst2 = singletonInst.val;int main(int argc, char** argv)
{std::cout << "get value: " << singletonInst2 << '\n';std::cout << singletonInst.val << std::endl;
}

这里将singletonInst实例在sing.h中完成初始化,这样不会出现mian.cpp调用实例,但是实例没有初始化构建的问题。

实现方式评注与思考

  1. place new与new的区别
    在这里插入图片描述

  2. reinterpret_cast的用法, 强制转换是在编译期完成的还是运行期?
    对于大多数情况来说,reinterpret_cast 操作符是在编译期完成的,但是在一些特殊情况下,比如使用了动态内存分配或者其它运行期的特性时,reinterpret_cast 的结果可能会在运行期才被确定。

下一品设计的思考

下一品将会完成 CRTP的设计思路,将单例逻辑与功能逻辑分开。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/526645.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

第十一篇 - 应用于市场营销视频场景中的人工智能和机器学习技术 – Video --- 我为什么要翻译介绍美国人工智能科技巨头IAB公司(1)

IAB平台&#xff0c;使命和功能 IAB成立于1996年&#xff0c;总部位于纽约市。 作为美国的人工智能科技巨头社会媒体和营销专业平台公司&#xff0c;互动广告局&#xff08;IAB- the Interactive Advertising Bureau&#xff09;自1996年成立以来&#xff0c;先后为700多家媒体…

Java 汇编源码查看环境搭建

目录 一、简介 二、在IDEA开发环境中搭建汇编环境 2.1 在IDEA中搭建字节码查看环境 2.1.1 搭建步骤 2.1.1.1 第一步 2.1.1.2 第二步 2.1.1.3 第三步 2.1.1.4 第四步 2.1.2 验证 2.2 在IDEA开发环境中搭建汇编代码查看环境 2.2.2 配置HSDIS插件 2.2.3 验证HSDIS插件是…

《vtk9 book》 官方web版 第3章 - 计算机图形基础 (3 / 6)

3.8 演员几何 我们已经看到了光照属性如何控制演员的外观&#xff0c;以及相机如何结合变换矩阵将演员投影到图像平面上。剩下的是定义演员的几何形状&#xff0c;以及如何将其定位在世界坐标系中。 建模 计算机图形学研究中的一个重要主题是建模或表示物体的几何形状。…

手机短信恢复 - 如何在 Android 手机上恢复删除的短信

手机每天发送和接收的短信数以亿计&#xff0c;而Android消息丢失每天都在发生。 Android短信恢复对于那些在设备中保存了一些重要信息的人来说显得至关重要。首先&#xff0c;我们向您推荐奇客数据恢复安卓版&#xff0c;这款软件能够高效、安全地恢复已删除的短信&#xff0…

真与假和对与错

我说过先看到股市里的真和假&#xff0c;再去管对和错。今天正好看到一个帖子写的这段话&#xff0c;我太认同了。你以为龙头是真的&#xff0c;股票是真的&#xff0c;其实都是假的&#xff0c;反而看不见的情绪是真的&#xff0c;情绪周期是真的&#xff0c;我们关注买在分歧…

MACBOOK PRO M2 MAX 安装Stable Diffusion及文生图实例

以前偶尔会使用Midjourney生成一些图片&#xff0c;现在使用的头像就是当时花钱在Midjourney上生成的。前段时间从某鱼上拍了一台性价比还不错的macbook&#xff0c;想着不如自己部署Stable Diffusion&#xff08;以下简称SD&#xff09;尝试一下。 网上有很多教程&#xff0c…

【网络原理】使用Java基于TCP搭建简单客户端与服务器通信

目录 &#x1f384;API介绍&#x1f338;ServerSocket API&#x1f338;Socket API &#x1f340;TCP中的长短连接&#x1f333;建立TCP回显客户端与服务器&#x1f338;TCP搭建服务器&#x1f338;TCP搭建客户端 ⭕总结 TCP服务器与客户端的搭建需要借助以下API &#x1f384;…

[Java安全入门]三.CC1链

1.前言 Apache Commons Collections是一个扩展了Java标准库里的Collection结构的第三方基础库&#xff0c;它提供了很多强大的数据结构类型和实现了各种集合工具类。Commons Collections触发反序列化漏洞构造的链叫做cc链&#xff0c;构造方式多种&#xff0c;这里先学习cc1链…

spring boot 2.4.x 之前版本(对应spring-cloud-openfeign 3.0.0之前版本)feign请求异常逻辑

目录 feign SynchronousMethodHandler 第一部分 第二部分 第三部分 spring-cloud-openfeign LoadBalancerFeignClient ribbon AbstractLoadBalancerAwareClient 在之前写的文章配置基础上 https://blog.csdn.net/zlpzlpzyd/article/details/136060312 因为从 spring …

Linux:kubernetes(k8s)prestop事件的使用(10)

他的作用是在结束pod容器之后进行的操作 apiVersion: v1 # api文档版本 kind: Pod # 资源对象类型 metadata: # pod相关的元数据&#xff0c;用于描述pod的数据name: nginx-po # pod名称labels: # pod的标签type: app #这个是随便写的 自定义的标签version: 1.0.0 #这个…

CTF-PWN-工具配置

pwngdb和pwngdb github上下载pwndbg和pwngdb 最后设置主目录下的.gdbinit文件 source /home/llk/tools/pwndbg/gdbinit.py #pwndbg/gdbinit.py位置而已 source ~/Pwngdb/pwngdb.py source ~/Pwngdb/angelheap/gdbinit.pydefine hook-run python import angelheap angelheap…

亲测:腾讯云8核16G服务器价格1668元一年送3个月,购买需谨慎

腾讯云8核16G轻量服务器CPU性能如何&#xff1f;18M带宽支持多少人在线&#xff1f;轻量应用服务器具有100%CPU性能&#xff0c;18M带宽下载速度2304KB/秒&#xff0c;折合2.25M/s&#xff0c;系统盘为270GB SSD盘&#xff0c;月流量3500GB&#xff0c;折合每天116.6GB流量&…