參考:http://blog.yangyubo.com/2009/06/04/best-cpp-singleton-pattern/
索引
我非常贊成合理的使用 設計模式 能讓代碼更容易理解和維護, 不過我自己除了簡單的 單例 (Singleton) 模式 外, 其它都很少用 :-)
可恥的是, 直到前段時間拜讀了 C++ In Theory: The Singleton Pattern, Part I, 我才發現自己的 單例 (Singleton) 模式 寫法還有改進空間.
文章作者 J. Nakamura 以 Log 日志類列舉了 單例 (Singleton) 模式 的三種寫法:
// log.h #ifndef __LOG_H #define __LOG_H #include <list> #include <string> class Log { public: virtual void Write(char const *logline); virtual bool SaveTo(char const *filename); private: std::list<std::string> m_data; }; #endif // __LOG_H
靜態化並不是單例 (Singleton) 模式
初學者可能會犯的錯誤, 誤以為把所有的成員變量和成員方法都用 static 修飾后, 就是單例模式了:
class Log { public: static void Write(char const *logline); static bool SaveTo(char const *filename); private: static std::list<std::string> m_data; }; In log.cpp we need to add std::list<std::string> Log::m_data;
乍一看確實具備單例模式的很多條件, 不過它也有一些問題. 第一, 靜態成員變量初始化順序不依賴構造函數, 得看編譯器心情的, 沒法保證初始化順序 (極端情況: 有 a b 兩個成員對象, b 需要把 a 作為初始化參數傳入, 你的類就 必須 得要有構造函數, 並確保初始化順序).
第二, 最嚴重的問題, 失去了面對對象的重要特性 -- "多態", 靜態成員方法不可能是 virtual 的. Log 類的子類沒法享受 "多態" 帶來的便利.
餓漢模式
餓漢模式 是指單例實例在程序運行時被立即執行初始化:
class Log { public: static Log* Instance() { return &m_pInstance; } virtual void Write(char const *logline); virtual bool SaveTo(char const *filename); private: Log(); // ctor is hidden Log(Log const&); // copy ctor is hidden static Log m_pInstance; static std::list<std::string> m_data; }; // in log.cpp we have to add Log Log::m_pInstance;
這種模式的問題也很明顯, 類現在是多態的, 但靜態成員變量初始化順序還是沒保證.
還引起另外一個問題 (我之前碰到過的真實事件, 以后便一直采用下面提到的 "懶漢模式"): 有兩個單例模式的類 ASingleton 和 BSingleton, 某天你想在 BSingleton 的構造函數中使用 ASingleton 實例, 這就出問題了. 因為 BSingleton m_pInstance 靜態對象可能先 ASingleton 一步調用初始化構造函數, 結果 ASingleton::Instance() 返回的就是一個未初始化的內存區域, 程序還沒跑就直接崩掉.
懶漢模式 (堆棧-粗糙版)
J. Nakamura 把它叫作 "Gamma Singleton", 因為這是 Gamma 在他大名鼎鼎的 <<設計模式>> (<<Design Patterns>>) [Gamma] 一書采用的方法. 稱它為 "懶漢模式" 是因為單例實例只在第一次被使用時進行初始化:
class Log { public: static Log* Instance() { if (!m_pInstance) m_pInstance = new Log; return m_pInstance; } virtual void Write(char const *logline); virtual bool SaveTo(char const *filename); private: Log(); // ctor is hidden Log(Log const&); // copy ctor is hidden static Log* m_pInstance; static std::list<std::string> m_data; }; // in log.cpp we have to add Log* Log::m_pInstance = NULL;
Instance() 只在第一次被調用時為 m_pInstance 分配內存並初始化. 嗯, 看上去所有的問題都解決了, 初始化順序有保證, 多態也沒問題.
不過細心的你可能已經發現了一個問題, 程序退出時, 析構函數沒被執行. 這在某些設計不可靠的系統上會導致資源泄漏, 比如文件句柄, socket 連接, 內存等等. 幸好 Linux / Windows 2000/XP 等常用系統都能在程序退出時自動釋放占用的系統資源. 不過這仍然可能是個隱患, 至少 J. Nakamura 印象中, 有些系統是不會自動釋放的.
對於這個問題, 比較土的解決方法是, 給每個 Singleton 類添加一個 destructor() 方法:
virtual bool destructor() { // ... release resource if (NULL!= m_pInstance) { delete m_pInstance; m_pInstance = NULL; } }
然后在程序退出時確保調用了每個 Singleton 類的 destructor() 方法, 這么做雖然可靠, 但卻很是繁瑣. 幸運的是, Meyers 大師有個更簡便的方法.
懶漢模式 (局部靜態變量-最佳版)
它也被稱為 Meyers Singleton [Meyers]:
class Log { public: static Log& Instance() { static Log theLog; return theLog; } virtual void Write(char const *logline); virtual bool SaveTo(char const *filename); private: Log(); // ctor is hidden Log(Log const&); // copy ctor is hidden Log& operator=(Log const&); // assign op is hidden static std::list<std::string> m_data; };
在 Instance() 函數內定義局部靜態變量的好處是, theLog `` 的構造函數只會在第一次調用 ``Instance() 時被初始化, 達到了和 "堆棧版" 相同的動態初始化效果, 保證了成員變量和 Singleton 本身的初始化順序.
它還有一個潛在的安全措施, Instance() 返回的是對局部靜態變量的引用, 如果返回的是指針, Instance() 的調用者很可能會誤認為他要檢查指針的有效性, 並負責銷毀. 構造函數和拷貝構造函數也私有化了, 這樣類的使用者不能自行實例化.
另外, 多個不同的 Singleton 實例的析構順序與構造順序相反.
范例代碼和注意事項 (最優實現)
把下面 C++ 代碼片段中的 Singleton 替換成實際類名, 快速得到一個單例類:
class Singleton { public: static Singleton& Instance() { static Singleton theSingleton; return theSingleton; } /* more (non-static) functions here */ private: Singleton(); // ctor hidden Singleton(Singleton const&); // copy ctor hidden Singleton& operator=(Singleton const&); // assign op. hidden ~Singleton(); // dtor hidden };
Note
-
任意兩個 Singleton 類的構造函數不能相互引用對方的實例, 否則會導致程序崩潰. 如:
ASingleton& ASingleton::Instance() { const BSingleton& b = BSingleton::Instance(); static ASingleton theSingleton; return theSingleton; } BSingleton& BSingleton::Instance() { const ASingleton & b = ASingleton::Instance(); static BSingleton theSingleton; return theSingleton; }
-
在多線程的應用場合下必須小心使用. 如果唯一實例尚未創建時, 有兩個線程同時調用創建方法, 且它們均沒有檢測到唯一實例的存在, 便會同時各自創建一個實例, 這樣就有兩個實例被構造出來, 從而違反了單例模式中實例唯一的原則. 解決這個問題的辦法是為指示類是否已經實例化的變量提供一個互斥鎖 (雖然這樣會降低效率).
-
多個 Singleton 實例相互引用的情況下, 需要謹慎處理析構函數. 如: 初始化順序為 ASingleton » BSingleton » CSingleton 的三個 Singleton 類, 其中 ASingleton BSingleton 的析構函數調用了 CSingleton 實例的成員函數, 程序退出時, CSingleton 的析構函數 將首先被調用, 導致實例無效, 那么后續 ASingleton BSingleton 的析構都將失敗, 導致程序異常退出.
擴展閱讀
- 反模式 : 在實踐中明顯出現但又低效或是有待優化的設計模式, 是用來解決問題的帶有共同性的不良方法. 它們已經經過研究並分類, 以防止日后重蹈覆轍, 並能在研發尚未投產的系統時辨認出來. (其中的一些反模式還挺有意思的);
- C++ In Theory: The Singleton Pattern, Part 2 : "C++ In Theory" 系列的第二部分, 主要內容是泛型編程中的單例模式, 我對泛型不太感冒, 感興趣的朋友可以看看.