1、在GCC4.0之后的環境下:
#include <iostream>
using namespace std;
template <typename T>
class Singleton
{
public:
static T& getInstance() {
//使用局部靜態變量的缺陷就是創建和析構時的不確定性。由於Singleton實例會在Instance()函數被訪問時被創建,因此在某處新添加的一處對Singleton的訪問將可能導致Singleton的生存期發生變化。如果其依賴於其它組成,如另一個Singleton,那么對它們的生存期進行管理將成為一個災難。
//甚至可以說,還不如不用Singleton,而使用明確的實例生存期管理。”
// Lock(); GCC 4.0以上的編譯器保證了內部靜態變量的線程安全,可以不需要這句話
//為什么c++0X之前需要加Lock,這是由局部靜態變量的實際實現所決定的。
//為了能滿足局部靜態變量只被初始化一次的需求
//,很多編譯器會通過一個全局的標志位記錄該靜態變量是否已經被初始化的信息。
//那么,對靜態變量進行初始化的偽碼就變成下面這個樣子:
//bool flag = false;
//if (!flag)
// {
// flag = true;
// staticVar = initStatic();
// }
static T s;
// UnLock()
cout << "new s" << endl;
return s; //如果返回的是指針可能會有被外部調用者delete掉的隱患,所以這里返回引用會更加保險一些。
}
private:
Singleton() {}
~Singleton(){}
Singleton(const Singleton& other) {}
Singleton& operator = (const Singleton& other) {}
};
class Eager_Singleton //餓漢模式
{
private:
Eager_Singleton() {
}
~Eager_Singleton(){
}
Eager_Singleton(const Eager_Singleton& other);
Eager_Singleton& operator = (const Eager_Singleton& other);
private:
static Eager_Singleton s; //在程序開始時進入主函數之前就由主線程以單線程方式執行該語句完成了初始化,
public:
static Eager_Singleton& getInstance() {
return s;
}
};
//將Singleton作為一個組件供其他類使用
class SingletonInstance : public Singleton<SingletonInstance> {
};
int main() {
Singleton<Singleton>::getInstance();
Eager_Singleton::getInstance();
return 0;
}
2、在GCC4.0之前,不用鎖實現
template<typename T>
class Singleton : boost::noncopyable
{
public:
static T& instance()
{
pthread_once(&ponce_, &Singleton::init);
return *value_;
}
static void init()
{
value_ = new T();
}
private:
static pthread_once_t ponce_;
static T* value_;
};
template<typename T>
pthread_once_t Singleton<T>::ponce_ = PTHREAD_ONCE_INIT;
template<typename T>
T* Singleton<T>::value_ = NULL;
3、在GCC4.0之前,用鎖實現(轉自write pattern line by line的一個面試場景)
template <typename T>
class Singleton
{
public:
static T& Instance()
{
if (m_pInstance == NULL)
{
Lock lock;
if (m_pInstance == NULL)
{
m_pInstance = new T();
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;
因為new運算符的調用分為分配內存、調用構造函數以及為指針賦值三步,就像下面的構造函數調用:”
1 SingletonInstance pInstance = new SingletonInstance();
“這行代碼會轉化為以下形式:”
1 SingletonInstance pHeap = __new(sizeof(SingletonInstance)); 2 pHeap->SingletonInstance::SingletonInstance(); 3 SingletonInstance pInstance = pHeap;
“這樣轉換是因為在C++標准中規定,如果內存分配失敗,或者構造函數沒有成功執行, new運算符所返回的將是空。一般情況下,編譯器不會輕易調整這三步的執行順序,但是在滿足特定條件時,如構造函數不會拋出異常等,編譯器可能出於優化的目的將第一步和第三步合並為同一步:”
1 SingletonInstance pInstance = __new(sizeof(SingletonInstance)); 2 pInstance->SingletonInstance::SingletonInstance();
“這樣就可能導致其中一個線程在完成了內存分配后就被切換到另一線程,而另一線程對Singleton的再次訪問將由於pInstance已經賦值而越過if分支,從而返回一個不完整的對象。因此,我在這個實現中為靜態成員指針添加了volatile關鍵字。該關鍵字的實際意義是由其修飾的變量可能會被意想不到地改變,因此每次對其所修飾的變量進行操作都需要從內存中取得它的實際值。它可以用來阻止編譯器對指令順序的調整。只是由於該關鍵字所提供的禁止重排代碼是假定在單線程環境下的,因此並不能禁止多線程環境下的指令重排。”
“最后來說說我對atexit()關鍵字的使用。在通過new關鍵字創建類型實例的時候,我們同時通過atexit()函數注冊了釋放該實例的函數,從而保證了這些實例能夠在程序退出前正確地析構。該函數的特性也能保證后被創建的實例首先被析構。其實,對靜態類型實例進行析構的過程與前面所提到的在main()函數執行之前插入靜態初始化邏輯相對應。”
引用還是指針
“既然你在實現中使用了指針,為什么仍然在Instance()函數中返回引用呢?”面試官又拋出了新的問題。
“這是因為Singleton返回的實例的生存期是由Singleton本身所決定的,而不是用戶代碼。我們知道,指針和引用在語法上的最大區別就是指針可以為NULL,並可以通過delete運算符刪除指針所指的實例,而引用則不可以。由該語法區別引申出的語義區別之一就是這些實例的生存期意義:通過引用所返回的實例,生存期由非用戶代碼管理,而通過指針返回的實例,其可能在某個時間點沒有被創建,或是可以被刪除的。但是這兩條Singleton都不滿足,因此在這里,我使用指針,而不是引用。”
“指針和引用除了你提到的這些之外,還有其它的區別嗎?”
“有的。指針和引用的區別主要存在於幾個方面。從低層次向高層次上來說,分為編譯器實現上的,語法上的以及語義上的區別。就編譯器的實現來說,聲明一個引用並沒有為引用分配內存,而僅僅是為該變量賦予了一個別名。而聲明一個指針則分配了內存。這種實現上的差異就導致了語法上的眾多區別:對引用進行更改將導致其原本指向的實例被賦值,而對指針進行更改將導致其指向另一個實例;引用將永遠指向一個類型實例,從而導致其不能為NULL,並由於該限制而導致了眾多語法上的區別,如dynamic_cast對引用和指針在無法成功進行轉化時的行為不一致。而就語義而言,前面所提到的生存期語義是一個區別,同時一個返回引用的函數常常保證其返回結果有效。一般來說,語義區別的根源常常是語法上的區別,因此上面的語義區別僅僅是列舉了一些例子,而真正語義上的差別常常需要考慮它們的語境。”
“你在前面說到了你的多線程內部實現使用了指針,而返回類型是引用。在編寫過程中,你是否考慮了實例構造不成功的情況,如new運算符運行失敗?”
“是的。在和其它人進行討論的過程中,大家對於這種問題有各自的理解。首先,對一個實例的構造將可能在兩處拋出異常:new運算符的執行以及構造函數拋出的異常。對於new運算符,我想說的是幾點。對於某些操作系統,例如Windows,其常常使用虛擬地址,因此其運行常常不受物理內存實際大小的限制。而對於構造函數中拋出的異常,我們有兩種策略可以選擇:在構造函數內對異常進行處理,以及在構造函數之外對異常進行處理。在構造函數內對異常進行處理可以保證類型實例處於一個有效的狀態,但一般不是我們想要的實例狀態。這樣一個實例會導致后面對它的使用更為繁瑣,例如需要更多的處理邏輯或再次導致程序執行異常。反過來,在構造函數之外對異常進行處理常常是更好的選擇,因為軟件開發人員可以根據產生異常時所構造的實例的狀態將一定范圍內的各個變量更改為合法的狀態。舉例來說,我們在一個函數中嘗試創建一對相互關聯的類型實例,那么在一個實例的構造函數拋出了異常時,我們不應該在構造函數里對該實例的狀態進行維護,因為前一個實例的構造是按照后一個實例會正常創建來進行的。相對來說,放棄后一個實例,並將前一個實例刪除是一個比較好的選擇。”
我在白板上比划了一下,繼續說到:“我們知道,異常有兩個非常明顯的缺陷:效率,以及對代碼的污染。在太小的粒度中使用異常,就會導致異常使用次數的增加,對於效率以及代碼的整潔型都是傷害。同樣地,對拷貝構造函數等組成常常需要使用類似的原則。”
“反過來說,Singleton的使用也可以保持着這種原則。Singleton僅僅是一個包裝好的全局實例,對其的創建如果一旦不成功,在較高層次上保持正常狀態同樣是一個較好的選擇。”
Anti-Patten
“既然你提到了Singleton僅僅是一個包裝好的全局變量,那你能說說它和全局變量的相同與不同么?”
“單件可以說是全局變量的替代品。其擁有全局變量的眾多特點:全局可見且貫穿應用程序的整個生命周期。除此之外,單件模式還擁有一些全局變量所不具有的性質:同一類型的對象實例只能有一個,同時適當的實現還擁有延遲初始化(Lazy)的功能,可以避免耗時的全局變量初始化所導致的啟動速度不佳等問題。要說明的是,Singleton的最主要目的並不是作為一個全局變量使用,而是保證類型實例有且僅有一個。它所具有的全局訪問特性僅僅是它的一個副作用。但正是這個副作用使它更類似於包裝好的全局變量,從而允許各部分代碼對其直接進行操作。軟件開發人員需要通過仔細地閱讀各部分對其進行操作的代碼才能了解其真正的使用方式,而不能通過接口得到組件依賴性等信息。如果Singleton記錄了程序的運行狀態,那么該狀態將是一個全局狀態。各個組件對其進行操作的調用時序將變得十分重要,從而使各個組件之間存在着一種隱式的依賴。”
“從語法上來講,首先Singleton模式實際上將類型功能與類型實例個數限制的代碼混合在了一起,違反了SRP。其次Singleton模式在Instance()函數中將創建一個確定的類型,從而禁止了通過多態提供另一種實現的可能。”
“但是從系統的角度來講,對Singleton的使用則是無法避免的:假設一個系統擁有成百上千個服務,那么對它們的傳遞將會成為系統的一個災難。從微軟所提供的眾多類庫上來看,其常常提供一種方式獲得服務的函數,如GetService()等。另外一個可以減輕Singleton模式所帶來不良影響的方法則是為Singleton模式提供無狀態或狀態關聯很小的實現。”
“也就是說,Singleton本身並不是一個非常差的模式,對其使用的關鍵在於何時使用它並正確的使用它。”