Design Patterns in Modern C++: The Singleton is the most hated design pattern in the (rather limited) history of design patterns. Just stating that, however, doesn’t mean you shouldn’t use the singleton: a toilet brush is not the most pleasant device either, but sometimes it’s simply necessary
《全唐诗》:夫物有所用,用之各有宜
懒汉模式:时间换空间,只有当第一次使用到类的实例时,才会初始化
懒汉模式所需解决的最主要问题是 线程安全
其原因在于,当有多个线程同时执行至 new LazySingleton()
所在的程序段时,instance
实例可能会被多次 new
class LazySingleton {
private:
static LazySingleton *instance;
LazySingleton() {};
public:
static auto GetInstance() -> LazySingleton * {
if (instance == nullptr) {
// new 新实例的时候,线程不安全
instance = new LazySingleton();
}
return instance;
};
};
LazySingleton* LazySingleton::instance = nullptr;
饿汉模式:空间换时间,加载进程时完成静态成员创建,
创建好的实例可能程序自始至终都未使用过,会造成资源浪费
class EagerSingleton {
private:
EagerSingleton() {};
static EagerSingleton * instance;
public:
static EagerSingleton* GetInstance() {
return instance;
}
};
EagerSingleton* EagerSingleton::instance = new EagerSingleton();
在 c++ 早期标准中,为了实现线程安全的懒汉式单例需要使用双校验锁机制,如下例:
class LazySingleton {
private:
static LazySingleton *instance;
static std::mutex latch_;
LazySingleton() {};
public:
static auto GetInstanceWithLock() -> LazySingleton *;
};
auto LazySingleton::GetInstanceWithLock() -> LazySingleton * {
if (LazySingleton::instance == nullptr) {
std::lock_guard<std::mutex> lock(latch_);
// 锁后仍需再次判断
if (LazySingleton::instance == nullptr)
instance = new LazySingleton();
}
return instance;
}
LazySingleton* LazySingleton::instance = nullptr;
std::mutex LazySingleton::latch_;
当实例被大量访问时,频繁的条件判断和加解锁会影响效率,好在在 c++11 及以后的版本,可以通过 local static
代替 双校验锁, 在下述代码中展示了在 栈 和 堆 上的两种实现 get()
和 getPointer()
,此外还可以通过 添加 thread_local
实现线程局部单例,即每线程单例
struct Database {
protected:
/**
* 隐藏构造函数,防止创建新实例
*/
Database() { /* do something */ };
public:
/**
* 懒汉式
* Meyer's Singleton 的经典模式
* C++11 即以后版本中,函数中的 local static 对象 只有第一次访问才会创建对象,并且线程安全
* 在 C++ 早期版本,可能需要双重校验锁来解决线程安全问题
*/
static Database& get() {
static Database database;
return database;
}
/**
* 我认为下面到代码可能导致内存泄露,书中提到在 Database 不会被析构这一前提下无泄漏
*
* 书中描述如下:
* The preceding implementation relies on the assumption that Database lives until the end of the program and the
* use of a pointer instead of a reference ensures that a destructor, even if you make one (which, if you do,
* would have to be public), is never called. And no, the preceding code doesn’t cause a memory leak.
*
* 下面到代码实现依赖于 "Database一直存在到程序结束" 这一假设。
* 使用指针而不是引用可以确保析构函数永远不会被调用,即使定义了析构函数。
* 这段代码不会导致内存泄漏
*
* 或许采用 share_ptr 就没必要担心这个问题了
*/
static Database& getPointer() {
static auto* database_p = new Database();
return *database_p;
}
/**
* 线程局部单例,不必担心线程安全问题,因为每个线程都有一个 thread_local 对象
* 在 Thread Information Block 中存储 一个 thread_local 副本
*/
static Database& getPerThread() {
thread_local Database database;
return database;
}
/**
* delete 拷贝构造、移动构造、=操作符等, 阻止创建新实例的可能性
* 在c++11 之前可以通过将拷贝构造、赋值运算 设为私有 也可以实现同样的目的
*/
Database(Database const&) = delete;
Database(Database&&) = delete;
Database& operator=(Database const&) = delete;
Database& operator=(Database &&) = delete;
};
此外,还需注意,上述代码中通过隐藏、禁止 Big Five 以减少创建新实例的可能性
对于 c++11 标准之前的代码,可以通过将 Big Three 设为私有函数代替之
实现单例模式具有诸多不便,例如当我们由于某种需求不在使用某个类作为单例,这会给使用者带来麻烦
好在 boost::di
依赖注入框架可以帮助我们便捷使用
auto injector = di::make_injector(
// 每当需要使用IFoo类型的组件时,都通过使用Foo类型的单例实例来初始化IFoo类型组件
di::bind<IFoo>().to<Foo>().in(di::singleton)
);
#include "boost/di.hpp"
#include "boost/lexical_cast.hpp"
#include "iostream"
namespace di = boost::di;
struct IFoo
{
virtual std::string name() = 0;
};
struct Foo : IFoo
{
static int id;
Foo() { ++id; }
std::string name() override
{
return "foo " + boost::lexical_cast<std::string>(id);
}
};
int Foo::id = 0;
struct Bar
{
std::shared_ptr<IFoo> foo;
};
int main()
{
auto injector = di::make_injector(
di::bind<IFoo>().to<Foo>().in(di::singleton)
);
// 获取Bar实例
auto bar1 = injector.create<std::shared_ptr<Bar>>();
auto bar2 = injector.create<std::shared_ptr<Bar>>();
std::cout << bar1->foo->name() << std::endl; // foo 1
std::cout << bar2->foo->name() << std::endl; // foo 1
std::cout << std::boolalpha
<< (bar1->foo.get() == bar2->foo.get()) // true
<< std::endl;
}
Design Patterns in Modern C++: According to many people, using a singleton in a DI container is the only socially acceptable use of a singleton. At least with this approach, if you need to replace a singleton object with something else, you can do it in one central place: the container configuration code. An added benefit is that you won’t have to implement any singleton logic yourself, which prevents possible errors. Oh, and did I mention that Boost.DI is thread safe?
Boost::di
是唯一可以接受的实现单例方式Boost::di
的线程安全性?作为一个尚未接触工业开发的学习者,并不清楚工业界实现单例模式的方式,但可以对其线程安全性进行简单分析: 从复杂的模板化源码中抽丝剥茧,重点关注 create()
以及 create_impl()
,不难发现 在创建一个新的实例时,采取的方式也是 Meyer’s Singleton,因此其是线程安全的,要归功于 local static
class singleton {
template <class T, class = decltype(aux::has_shared_ptr__(aux::declval<T>()))>
class scope_impl {
public:
template <class T_, class>
using is_referable = typename wrappers::shared<singleton, T&>::template is_referable<T_>;
template <class, class, class TProvider>
static decltype(wrappers::shared<singleton, T&>{aux::declval<TProvider>().get(type_traits::stack{})}) try_create(
const TProvider&);
template <class, class, class TProvider>
auto create(const TProvider& provider) {
return create_impl(provider);
}
private:
// Meyer's Singleton
template <class TProvider>
wrappers::shared<singleton, T&> create_impl(const TProvider& provider) {
static auto object(provider.get(type_traits::stack{}));
return wrappers::shared<singleton, T&>(object);
}
};
单态模式是单例模式的一种变体,行为上类似单例模式
class Printer {
static int id;
public:
int get_id() const { return id; }
void set_id(int value) { id = value; }
};
事实上这只是一个提供了 get()
, set()
方法的普通的类,不过他们操作的都是 静态数据
这种方式中,如果使用者可以妥善的处理不同的 Printer
对象实例,那么可以实现单例模式的功能
但当用户不断创建新实例,尝试操作 static
数据时,或许我们就会怀念 “the toilet brush”