引子html
class Singleton { public: static Singleton Instance() { static Singleton singleton; return singleton; } private: Singleton() { }; };
“那请你讲解一下该实现的各组成。”面试官的脸上仍然带着微笑。java
“首先要说的就是Singleton的构造函数。因为Singleton限制其类型实例有且只能有一个,所以咱们应经过将构造函数设置为非公有来保证其不会被用户代码随意建立。而在类型实例访问函数中,咱们经过局部静态变量达到实例仅有一个的要求。另外,经过该静态变量,咱们能够将该实例的建立延迟到实例访问函数被调用时才执行,以提升程序的启动速度。”面试
保护编程
“说得不错,并且更难得的是你能注意到对构造函数进行保护。毕竟中间件代码须要很是严谨才能防止用户代码的误用。那么,除了构造函数之外,咱们还须要对哪些组成进行保护?”安全
“还须要保护的有拷贝构造函数,析构函数以及赋值运算符。或许,咱们还须要考虑取址运算符。这是由于编译器会在须要的时候为这些成员建立一个默认的实现。”多线程
“那你能详细说一下编译器会在什么状况下建立默认实现,以及建立这些默认实现的缘由吗?”面试官继续问道。函数
“在这些成员没有被声明的状况下,编译器将使用一系列默认行为:对实例的构造就是分配一部份内存,而不对该部份内存作任何事情;对实例的拷贝也仅仅是将原实例中的内存按位拷贝到新实例中;而赋值运算符也是对类型实例所拥有的各信息进行拷贝。而在某些状况下,这些默认行为再也不知足条件,那么编译器将尝试根据已有信息建立这些成员的默认实现。这些影响因素能够分为几种:类型所提供的相应成员,类型中的虚函数以及类型的虚基类。”性能
“就以构造函数为例,若是当前类型的成员或基类提供了由用户定义的构造函数,那么仅进行内存拷贝可能已经不是正确的行为。这是由于该成员的构造函数可能bao含了成员初始化,成员函数调用等众多执行逻辑。此时编译器就须要为这个类型生成一个默认构造函数,以执行对成员或基类构造函数的调用。另外,若是一个类型声明了一个虚函数,那么编译器仍须要生成一个构造函数,以初始化指向该虚函数表的指针。若是一个类型的各个派生类中拥有一个虚基类,那么编译器一样须要生成构造函数,以初始化该虚基类的位置。这些状况一样须要在拷贝构造函数中考虑:若是一个类型的成员变量拥有一个拷贝构造函数,或者其基类拥有一个拷贝构造函数,位拷贝就再也不知足要求了,由于拷贝构造函数内可能执行了某些并非位拷贝的逻辑。同时若是一个类型声明了虚函数,拷贝构造函数须要根据目标类型初始化虚函数表指针。如基类实例通过拷贝后,其虚函数表指针不该指向派生类的虚函数表。同理,若是一个类型的各个派生类中拥有一个虚派生,那么编译器也应为其生成拷贝构造函数,以正确设置各个虚基类的偏移。”优化
“固然,析构函数的状况则略为简单一些:只须要调用其成员的析构函数以及基类的析构函数便可,而不须要再考虑对虚基类偏移的设置及虚函数表指针的设置。”spa
“在这些默认实现中,类型实例的各个原生类型成员并无获得初始化的机会。可是这通常被认为是软件开发人员的责任,而不是编译器的责任。”说完这些,我长出一口气,内心也暗自庆幸曾经研究过该部份内容。
“你刚才提到须要考虑保护取址运算符,是吗?我想知道。”
“好的。首先要声明的是,几乎全部的人都会认为对取址运算符的重载是邪恶的。甚至说,boost为了防止该行为所产生的错误更是提供了addressof()函数。而另外一方面,咱们须要讨论用户为何要用取址运算符。Singleton所返回的经常是一个引用,对引用进行取址将获得相应类型的指针。而从语法上来讲,引用和指针的最大区别在因而否能够被delete关键字删除以及是否能够为NULL。可是Singleton返回一个引用也就表示其生存期由非用户代码所管理。所以使用取址运算符得到指针后又用delete关键字删除Singleton所返回的实例明显是一个用户错误。综上所述,经过将取址运算符设置为私有没有多少意义。”
重用
“好的,如今咱们换个话题。若是我如今有几个类型都须要实现为Singleton,那我应怎样使用你所编写的这段代码呢?”
刚刚还在洋洋自得的我恍然大悟:这个Singleton实现是没法重用的。没办法,只好一边想一边说:“通常来讲,较为流行的重用方法一共有三种:组合、派生以及模板。首先能够想到的是,对Singleton的重用仅仅是对Instance()函数的重用,所以经过从Singleton派生以继承该函数的实现是一个很好的选择。而Instance()函数若是能根据实际类型更改返回类型则更好了。所以奇异递归模板(CRTP,The Curiously Recurring Template Pattern)模式则是一个很是好的选择。”因而我在白板上飞快地写下了下面的代码:
[cpp] view plain copy print?
template <typename T>
class Singleton
{
public:
static T& Instance()
{
static T s_Instance;
return s_Instance;
}
protected:
Singleton(void) {}
~Singleton(void) {}
private:
Singleton(const Singleton& rhs) {}
Singleton& operator = (const Singleton& rhs) {}
};
同时我也在白板上写下了对该Singleton实现进行重用的方法:
[cpp] view plain copy print?
class SingletonInstance : public Singleton<SingletonInstance>…
“在须要重用该Singleton实现时,咱们仅仅须要从Singleton派生并将Singleton的泛型参数设置为该类型便可。”
生存期管理
“我看你在实现中使用了静态变量,那你是否能介绍一下上面Singleton实现中有关生存期的一些特征吗?毕竟生存期管理也是编程中的一个重要话题。”面试官提出了下一个问ti。
“嗯,让我想想。我认为对Singleton的生存期特性的讨论须要分为两个方面:Singleton内使用的静态变量的生存期以及Singleton外在用户代码中所表现的生存期。Singleton内使用的静态变量是一个局部静态变量,所以只有在Singleton的Instance()函数被调用时其才会被建立,从而拥有了延迟初始化(Lazy)的效果,提升了程序的启动性能。同时该实例将生存至程序执行完毕。而就Singleton的用户代码而言,其生存期贯穿于整个程序生命周期,从程序启动开始直到程序执行完毕。固然,Singleton在生存期上的一个缺陷就是建立和析构时的不肯定性。因为Singleton实例会在Instance()函数被访问时被建立,所以在某处新添加的一处对Singleton的访问将可能致使Singleton的生存期发生变化。若是其依赖于其它组成,如另外一个Singleton,那么对它们的生存期进行管理将成为一个灾难。甚至能够说,还不如不用Singleton,而使用明确的实例生存期管理。”
“很好,你能提到程序初始化及关闭时单件的构造及析构顺序的不肯定可能致使致命的错误这一状况。能够说,这是经过局部静态变量实现Singleton的一个重要缺点。而对于你所提到的多个Singleton之间相互关联所致使的生存期管理问ti,你是否有解决该问ti的方法呢?”
我忽然间意识到本身给本身出了一个难ti:“有,咱们能够将Singleton的实现更改成使用全局静态变量,并将这些全局静态变量在文件中按照特定顺序排序便可。”
“可是这样的话,静态变量将使用eager initialization的方式完成初始化,可能会对性能影响较大。其实,我想听你说的是,对于具备关联的两个Singleton,对它们进行使用的代码经常局限在同一区域内。该问ti的一个解决方法经常是将对它们进行使用的管理逻辑实现为Singleton,而在内部逻辑中对它们进行明确的生存期管理。但不用担忧,由于这个da案也过于经验之谈。那么下一个问ti,你既然提到了全局静态变量能解决这个问ti,那是否能够讲解一下全局静态变量的生命周期是怎样的呢?”
“编译器会在程序的main()函数执行以前插入一段代码,用来初始化全局变量。固然,静态变量也bao含在内。该过程被称为静态初始化。”
“嗯,很好。使用全局静态变量实现Singleton的确会对性能形成必定影响。可是你是否注意到它也有必定的优势呢?”
见我许久没有回da,面试官主动帮我解了围:“是线程安全性。因为在静态初始化时用户代码尚未来得及执行,所以其经常处于单线程环境下,从而保证了Singleton真的只有一个实例。固然,这并非一个好的解决方法。因此,咱们来谈谈Singleton的多线程实现吧。”
多线程
“首先请你写一个线程安全的Singleton实现。”
我拿起笔,在白板上写下早已烂熟于心的多线程安全实现:
[cpp] view plain copy print?
template <typename T>
class Singleton
{
public:
static T& Instance()
{
if (m_pInstance == NULL)
{
Lock lock;
if (m_pInstance == NULL)
{
m_pInstance = new T();
atexit(Destroy);
}
return *m_pInstance;
}
return *m_pInstance;
}
protected:
Singleton(void) {}
~Singleton(void) {}
private:
Singleton(const Singleton& rhs) {}
Singleton& operator = (const Singleton& rhs) {}
void Destroy()
{
if (m_pInstance != NULL)
delete m_pInstance;
m_pInstance = NULL;
}
static T* volatile m_pInstance;
};
template <typename T>
T* Singleton<T>::m_pInstance = NULL;
“写得很精彩。那你是否能逐行讲解一下你写的这个Singleton实现呢?”
“好的。首先,我使用了一个指针记录建立的Singleton实例,而再也不是局部静态变量。这是由于局部静态变量可能在多线程环境下出现问ti。”
“我想插一句话,为何局部静态变量会在多线程环境下出现问题?”
“这是由局部静态变量的实际实现所决定的。为了能知足局部静态变量只被初始化一次的需求,不少编译器会经过一个全局的标志位记录该静态变量是否已经被初始化的信息。那么,对静态变量进行初始化的伪码就变成下面这个样子:”。
[cpp] view plain copy print?
bool flag = false;
if (!flag)
{
flag = true;
staticVar = initStatic();
}
“那么在第一个线程执行完对flag的检查并进入if分支后,第二个线程将可能被启动,从而也进入if分支。这样,两个线程都将执行对静态变量的初始化。所以在这里,我使用了指针,并在对指针进行赋值以前使用锁保证在同一时间内只能有一个线程对指针进行初始化。同时基于性能的考虑,咱们须要在每次访问实例以前检查指针是否已经通过初始化,以免每次对Singleton的访问都须要请求对锁的控制权。”
“同时,”我咽了口口水继续说,“由于new运算符的调用分为分配内存、调用构造函数以及为指针赋值三步,就像下面的构造函数调用:”
[cpp] view plain copy print?
SingletonInstance pInstance = new SingletonInstance();
“这行代码会转化为如下形式:”
[cpp] view plain copy print?
SingletonInstance pHeap = __new(sizeof(SingletonInstance));
pHeap->SingletonInstance::SingletonInstance();
SingletonInstance pInstance = pHeap;
“这样转换是由于在C++标准中规定,若是内存分配失败,或者构造函数没有成功执行, new运算符所返回的将是空。通常状况下,编译器不会轻易调整这三步的执行顺序,可是在知足特定条件时,如构造函数不会抛出异常等,编译器可能出于优化的目的将第一步和第三步合并为同一步:”
[html] view plain copy print?
SingletonInstance pInstance = __new(sizeof(SingletonInstance));
pInstance->SingletonInstance::SingletonInstance();
“这样就可能致使其中一个线程在完成了内存分配后就被切换到另外一线程,而另外一线程对Singleton的再次访问将因为pInstance已经赋值而越过if分支,从而返回一个不完整的对象。所以,我在这个实现中为静态成员指针添加了volatile关键字。该关键字的实际意义是由其修饰的变量可能会被意想不到地改变,所以每次对其所修饰的变量进行操做都须要从内存中取得它的实际值。它能够用来阻止编译器对指令顺序的调整。只是因为该关键字所提供的禁止重排代码是假定在单线程环境下的,所以并不能禁止多线程环境下的指令重排。”
“最后来讲说我对atexit()关键字的使用。在经过new关键字建立类型实例的时候,咱们同时经过atexit()函数注册了释放该实例的函数,从而保证了这些实例可以在程序退出前正确地析构。该函数的特性也能保证后被建立的实例首先被析构。其实,对静态类型实例进行析构的过程与前面所提到的在main()函数执行以前插入静态初始化逻辑相对应。”
引用仍是指针
“既然你在实现中使用了指针,为何仍然在Instance()函数中返回引用呢?”面试官又抛出了新的问ti。
“这是由于Singleton返回的实例的生存期是由Singleton自己所决定的,而不是用户代码。咱们知道,指针和引用在语法上的最大区别就是指针能够为NULL,并能够经过delete运算符删除指针所指的实例,而引用则不能够。由该语法区别引伸出的语义区别之一就是这些实例的生存期意义:经过引用所返回的实例,生存期由非用户代码管理,而经过指针返回的实例,其可能在某个时间点没有被建立,或是能够被删除的。可是这两条Singleton都不知足,所以在这里,我使用指针,而不是引用。”
“指针和引用除了你提到的这些以外,还有其它的区别吗?”
“有的。指针和引用的区别主要存在于几个方面。从低层次向高层次上来讲,分为编译器实现上的,语法上的以及语义上的区别。就编译器的实现来讲,声明一个引用并无为引用分配内存,而仅仅是为该变量赋予了一个别名。而声明一个指针则分配了内存。这种实现上的差别就致使了语法上的众多区别:对引用进行更改将致使其本来指向的实例被赋值,而对指针进行更改将致使其指向另外一个实例;引用将永远指向一个类型实例,从而致使其不能为NULL,并因为该限制而致使了众多语法上的区别,如dynamic_cast对引用和指针在没法成功进行转化时的行为不一致。而就语义而言,前面所提到的生存期语义是一个区别,同时一个返回引用的函数经常保证其返回结果有效。通常来讲,语义区别的根源经常是语法上的区别,所以上面的语义区别仅仅是列举了一些例子,而真正语义上的差异经常须要kao虑它们的语境。”
“你在前面说到了你的多线程内部实现使用了指针,而返回类型是引用。在编写过程当中,你是否kao虑了实例构造不成功的状况,如new运算符运行失败?”
“是的。在和其它人进行讨论的过程当中,你们对于这种问题有各自的理解。首先,对一个实例的构造将可能在两处抛出异常:new运算符的执行以及构造函数抛出的异常。对于new运算符,我想说的是几点。对于某些操做系统,例如Windows,其经常使用虚拟地址,所以其运行经常不受物理内存实际大小的限制。而对于构造函数中抛出的异常,咱们有两种策略能够选择:在构造函数内对异常进行处理,以及在构造函数以外对异常进行处理。在构造函数内对异常进行处理能够保证类型实例处于一个有效的状态,但通常不是咱们想要的实例状态。这样一个实例会致使后面对它的使用更为繁琐,例如须要更多的处理逻辑或再次致使程序执行异常。反过来,在构造函数以外对异常进行处理经常是更好的选择,由于软件开发人员能够根据产生异常时所构造的实例的状态将必定范围内的各个变量更改成合法的状态。举例来讲,咱们在一个函数中尝试建立一对相互关联的类型实例,那么在一个实例的构造函数抛出了异常时,咱们不该该在构造函数里对该实例的状态进行维护,由于前一个实例的构造是按照后一个实例会正常建立来进行的。相对来讲,放弃后一个实例,并将前一个实例删除是一个比较好的选择。”
我在白板上比划了一下,继续说到:“咱们知道,异常有两个很是明显的缺陷:效率,以及对代码的污染。在过小的粒度中使用异常,就会致使异常使用次数的增长,对于效率以及代码的整洁型都是伤害。一样地,对kao贝构造函数等组成经常须要使用相似的原则。”
“反过来讲,Singleton的使用也能够保持着这种原则。Singleton仅仅是一个bao装好的全局实例,对其的建立若是一旦不成功,在较高层次上保持正常状态一样是一个较好的选择。”
Anti-Patten
“既然你提到了Singleton仅仅是一个bao装好的全局变量,那你能说说它和全局变量的相同与不一样么?”
“单件能够说是全局变量的替代品。其拥有全局变量的众多特色:全局可见且贯穿应用程序的整个生命周期。除此以外,单件模式还拥有一些全局变量所不具备的性质:同一类型的对象实例只能有一个,同时适当的实现还拥有延迟初始化(Lazy)的功能,能够避免耗时的全局变量初始化所致使的启动速度不佳等问题。要说明的是,Singleton的最主要目的并非做为一个全局变量使用,而是保证类型实例有且仅有一个。它所具备的全局访问特性仅仅是它的一个反作用。但正是这个反作用使它更相似于bao装好的全局变量,从而容许各部分代码对其直接进行操做。软件开发人员须要经过仔细地阅读各部分对其进行操做的代码才能了解其真正的使用方式,而不能经过接口获得组件依赖性等信息。若是Singleton记录了程序的运行状态,那么该状态将是一个全局状态。各个组件对其进行操做的调用时序将变得十分重要,从而使各个组件之间存在着一种隐式的依赖。”
“从语法上来说,首先Singleton模式实际上将类型功能与类型实例个数限制的代码混合在了一块儿,违反了SRP。其次Singleton模式在Instance()函数中将建立一个肯定的类型,从而禁止了经过多态提供另外一种实现的可能。”
“可是从系统的角度来说,对Singleton的使用则是没法避免的:假设一个系统拥有成百上千个服务,那么对它们的传递将会成为系统的一个灾难。从微软所提供的众多类库上来看,其经常提供一种方式得到服务的函数,如GetService()等。另一个能够减轻Singleton模式所带来不良影响的方法则是为Singleton模式提供无状态或状态关联很小的实现。”
“也就是说,Singleton自己并非一个很是差的模式,对其使用的关键在于什么时候使用它并正确的使用它。”
面试官抬起手腕看了看时间:“好了,时间已经到了。你的C++功底已经很好了。我相信,咱们会在不久的未来成为同事。”