最简单的单例实现只需要一个全局对象:
some_class the_instance; some_class *get_instance() { return &the_instance; }
这个实现简单到没什么可说,它在绝大多数情况下能正常工作,之所以说是“绝大多数”,主要原因是:
1、它不能保证正确处理依赖性,也就是说,如果你有其它全局对象在构造时依赖于这个对象,你不能保证它能正常工作。
2、构造的时间点其实是不确定的,C++标准只要求这个对象在第一次被使用前,它的构造函数会被调用,具体实现中的保证甚至还达不到这个强度,绝大部分的实现只能保证main
之后的函数中使用这个对象时,构造函数会被调用。
3、这个实现可能会导致你实例化了根本没用到的单例,因为“用到”这个对象的行为不一定能被C++准确识别,很多实现只是简单的在main
之前按定义的顺序调用所有全局对象的构造函数。
所以,既然你在用C++11,那我们就用C++11的新特性来实现这个功能。
在C++11中引入了一个新的东西叫
call_once
,配合once_flag
,你可以保证一个函数只会被调用一次,有了这个东西,创建一个单例的程序可以简化成这样:some_class *the_instance; std::once_flag instance_created; some_class *get_instance() { std::call_once( instance_created, [](){ the_instance=new some_class(...); } ); return the_instance; }这段程序在多线程环境里也能正常工作,也能处理单例间的依赖,而且不用在意各种OS/编译器规定的各种坑爹初始化次序,任何编译器,只要它正确实现了C++11标准中的thread部分,就能保证这一点。
单例的销毁其实也是个挺烦问题,最主要的问题是你很难准确的知道你从什么时候开始才真得不再用它,一个简单的思路是
atexit
放一个析构函数在main
之后,但是正如你所预料,简单的实现往往有问题。POSIX的atexit不是线程安全的,你需要用std::atexit
才能保证线程安全。
但即使是std::atexit
也不能正确处理单件之间的依赖,所以我们需要点更精细的办法,比如引用计数。
引用计数最大的好处是对象的生存期真正的对应于对象的“有效期”,使用引用计数的版本变成了这样:std::shared_ptr<some_class> the_instance; std::once_flag instance_created; std::shared_ptr<some_class> get_instance() { std::call_once( instance_created, [](){ the_instance.reset(std::make_shared<some_class>(...)); } ); return the_instance; }
全局变量
std::shared_ptr<some_class> the_instance
保证一旦对象创建之后,直到程序结束,它至少有一个活引用,所以不会提前销毁;另外任何依赖于它的对象都会导致引用计数增加,所以可以正确的处理依赖性。
当然,这个实现的代价是API接口变成了std::shared_ptr<some_class>
,因为无论是裸指针还是引用,都不能正确的实现引用计数的语义,如果你对此还是不满意,可以用Boehm GC实现一个兼容裸指针和引用的版本。PS. LoadLibrary/dlopen最大的问题就是它们可能会跳过全局初始化的部分,也就是说,所有“逻辑上”应该在
main
之前执行的部分有可能根本就不会执行,由此会带来各种各样古怪的问题很难在这儿一一详述,总之,避免全局对象+LoadLibrary/dlopen这种组合会极大的改善你的生活品质。