C++ Singleton (單例) 模式最優實現

參考: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) 模式 的三種寫法:socket

// 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 修飾後, 就是單例模式了:ide

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 類的子類無法享受 "多態" 帶來的便利.優化

餓漢模式

餓漢模式 是指單例實例在程序運行時被當即執行初始化:spa

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;

這種模式的問題也很明顯, 類如今是多態的, 但靜態成員變量初始化順序仍是沒保證.線程

還引發另一個問題 (我以前碰到過的真實事件, 之後便一直採用下面提到的 "懶漢模式"): 有兩個單例模式的類 ASingletonBSingleton, 某天你想在 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" 系列的第二部分, 主要內容是泛型編程中的單例模式, 我對泛型不太感冒, 感興趣的朋友能夠看看.

參考資料

[Gamma] Design Patterns: E.Gamma, R.Helm, R.Johnson and J.Vlissides.
相關文章
相關標籤/搜索