单例九品--第九品[可用的设计]
- 上一品引入
- 写在前边
- 代码部分
- 实现方式的评注和思考
- 写在最后
上一品引入
自第五品以来,为解决第四品的静态初始化灾难问题,将全局对象设置为指针类型,但是指针是有被修改的风险。所以第八品将全局单例对象封装在sing类的内部,写为私有类型,并提供了一个调用接口,完成单例对象引用绑定。第八品的代码设计思路已经是可以使用的了,但是代码比较混乱复杂,不便于拓展。第九品将实现代码的易拓展性。
写在前边
- 基本思路
• 对单例类的功能逻辑与单例逻辑进行划分,分别放入不同的部分
• 使用CRTP模拟“基类-派生类”行为 - 优点
• 更容易支持不同的单例实例
• 使用模板不会引入运行期成本
代码部分
5个文件: sing_temp.h sing1.h sing2.h main.cpp src.cpp
- sing_temp.cpp
#pragma once
#include <atomic>template <typename T>
class SingTemp
{
public:struct Init{Init(){auto& count = RefCount();auto ori = count.fetch_add(1);if (ori == 0){T* ptr = SingTemp::Ptr(); // new (ptr) T(); // placement new的构造方式}}~Init(){auto& count = RefCount();auto ori = count.fetch_sub(1);if (ori == 1){T* ptr = SingTemp::Ptr();ptr->~T();}}static auto& RefCount(){static std::atomic<unsigned> count{ 0 };return count;}Init(const Init&) = delete;Init& operator= (const Init&) = delete;};protected:SingTemp() = default;~SingTemp() = default;SingTemp(const SingTemp&) = delete;SingTemp& operator= (const SingTemp&) = delete;public:static T* Ptr(){alignas(T) static char singBuf[sizeof(T)]; // 编译器知道有一块内存要分给char类型数组return reinterpret_cast<T*>(singBuf); // char类型数组指针强制转换为T*类,并返回这块内存的首地址。供派生类去引用绑定}
};
- sing1.h
#pragma once
#include "sing_temp.h"
#include <iostream>class Sing1 : public SingTemp<Sing1>
{friend SingTemp<Sing1>; // 为了基类中调用派生类的析构函数和构造函数,或者不写这句话,直接把构造函数和析构函数写成public(不安全)private:Sing1(): SingTemp<Sing1>(){std::cout << "Sing1 construct\n";val = 100;}~Sing1(){std::cout << "Sing1 destroy\n";}public:int val;
};static Sing1::Init sing1Init;
static Sing1& singleton1 = *Sing1::Ptr();
- sing2.h
#pragma once
#include "sing_temp.h"
#include "sing1.h"
#include <iostream>class Sing2 : public SingTemp<Sing2>
{friend SingTemp<Sing2>;private:Sing2(): SingTemp<Sing2>(){std::cout << "Sing2 construct\n";val = singleton1.val + 1; // 使用sing1类实现 sing2的构造初始化}~Sing2(){std::cout << "Sing2 destroy\n";}public:int val;
};static Sing2::Init sing2Init;
static Sing2& singleton2 = *Sing2::Ptr();
- main.cpp
#include "sing2.h"
#include "sing1.h"void fun();
int main(int argc, char** argv)
{std::cout << "from main: " << singleton1.val << '\n';std::cout << "from main: " << singleton2.val << '\n';fun();
}
- src.cpp
#include "sing1.h"
#include "sing2.h"
#include <iostream>void fun()
{std::cout << "from fun: " << singleton2.val << '\n';std::cout << "from fun: " << singleton1.val << '\n';
}
- output
Sing1 construct
Sing2 construct
from main: 100
from main: 101
from fun: 101
from fun: 100
Sing2 destroy
Sing1 destroy
实现方式的评注和思考
-
sing_temp.h是一个基类,sing1.h和sing2.h是基类的派生类。sing_temp.h中完成了初始化子类的构造逻辑,并提供了一个全局对象的静态函数访问接口,也就是完成了单例逻辑的封装。sing1.h和sing2.h封存了各自的功能逻辑。
-
派生类对象中将基类定义为派生类的友元类,因为基类中需要使用派生类中的析构函数和构造函数,如果不将基类定义为派生类的友元类,那么基类没有调用派生类中私有函数的权限。
-
基类sing_temp的init函数结合计数逻辑控制单例的初次构造时机和销毁时机,使用placement new的方式完成首次访问的单例(对应派生类)构造,并提供一个调用函数接口。其中placement new实现的实例构造和销毁的时候,分别调用的是对应派生类中的构造函数和析构函数,也就是说基类调用了派生类的私有成员函数(因为为了安全,析构函数和构造函数写为了私有)。 因此,需要在派生类sing.h中将基类写为友元类,这样基类才能调用派生类中的成员函数
-
CRTP模式实现了基类调用派生类中私有函数的方法。不使用CRTP方法的时候,一般的实现思路就是在基类中使用虚函数,然后在派生类中重写。通过CRTP可以使得类具有类似于虚函数的效果,同时又没有虚函数调用时的开销(虚函数调用需要通过虚函数指针查找虚函数表进行调用),同时类的对象的体积相比使用虚函数也会减少(不需要存储虚函数指针),但是缺点是无法动态绑定。
一个关奇异递归模板模式(Curiously Recurring Template Pattern)的帖子的链接
写在最后
第三品与第九品都是可以使用的设计思路,但是第三品因为每次使用到单例都要进入instance函数判断单例是否已经被构造,所以引入了多线程的时间损耗。第九品相对第三品而言,在本专栏的中的,如果在第三品和第九品的main函数加入下边的代码:
size_t res = 0;for (unsigned i = 0; i < 999999999; ++i){res += singleton1.val + i;}
就可以看出,第九品的运算次数是第三品运算次数的30%。性能上有了很大的提升。