C++ 單例模式總結與剖析
C++ 單例模式總結與剖析
單例可能是最常用的簡單的一種設計模式,實現方法多樣,根據不同的需求有不同的寫法; 同時單例也有其侷限性,因此有很多人是反對使用單例的。本文對C++ 單例的常見寫法進行了一個總結, 包括懶漢式、執行緒安全、單例模板等; 按照從簡單到複雜,最終迴歸簡單的的方式循序漸進地介紹,並且對各種實現方法的侷限進行了簡單的闡述,大量用到了C++ 11的特性如智慧指標, magic static,執行緒鎖; 從頭到尾理解下來,對於學習和鞏固C++語言特性還是很有幫助的。本文的全部程式碼在 g++ 5.4.0 編譯器下編譯執行通過,可以在我的github 倉庫 中找到。
一、什麼是單例
單例 Singleton 是設計模式的一種,其特點是隻提供唯一 一個類的例項,具有全域性變數的特點,在任何位置都可以通過介面獲取到那個唯一例項;
具體運用場景如:
- 裝置管理器,系統中可能有多個裝置,但是隻有一個裝置管理器,用於管理裝置驅動;
- 資料池,用來快取資料的資料結構,需要在一處寫,多處讀取或者多處寫,多處讀取;
二、C++單例的實現
2.1 基礎要點
- 全域性只有一個例項:static 特性,同時禁止使用者自己宣告並定義例項(把建構函式設為 private)
- 執行緒安全
- 禁止賦值和拷貝
- 使用者通過介面獲取例項:使用 static 類成員函式
2.2 C++ 實現單例的幾種方式
2.2.1 有缺陷的懶漢式
懶漢式(Lazy-Initialization)的方法是直到使用時才例項化物件,也就說直到呼叫get_instance() 方法的時候才 new 一個單例的物件。好處是如果被呼叫就不會佔用記憶體。
#include <iostream> // version1: // with problems below: // 1. thread is not safe // 2. memory leak class Singleton{ private: Singleton(){ std::cout<<"constructor called!"<<std::endl; } Singleton(Singleton&)=delete; Singleton& operator=(const Singleton&)=delete; static Singleton* m_instance_ptr; public: ~Singleton(){ std::cout<<"destructor called!"<<std::endl; } static Singleton* get_instance(){ if(m_instance_ptr==nullptr){ m_instance_ptr = new Singleton; } return m_instance_ptr; } void use() const { std::cout << "in use" << std::endl; } }; Singleton* Singleton::m_instance_ptr = nullptr; int main(){ Singleton* instance = Singleton::get_instance(); Singleton* instance_2 = Singleton::get_instance(); return 0; }
執行的結果是
constructor called!
可以看到,獲取了兩次類的例項,卻只有一次類的建構函式被呼叫,表明只生成了唯一例項,這是個最基礎版本的單例實現,他有哪些問題呢?
-
執行緒安全的問題
,當多執行緒獲取單例時有可能引發競態條件:第一個執行緒在if中判斷
m_instance_ptr
是空的,於是開始例項化單例;同時第2個執行緒也嘗試獲取單例,這個時候判斷m_instance_ptr
還是空的,於是也開始例項化單例;這樣就會例項化出兩個物件,這就是執行緒安全問題的由來;解決辦法 :加鎖 - 記憶體洩漏 . 注意到類中只負責new出物件,卻沒有負責delete物件,因此只有建構函式被呼叫,解構函式卻沒有被呼叫;因此會導致記憶體洩漏。解決辦法 : 使用共享指標;
因此,這裡提供一個改進的,執行緒安全的、使用智慧指標的實現;
2.2.2 執行緒安全、記憶體安全的懶漢式單例 (智慧指標,鎖)
#include <iostream> #include <memory> // shared_ptr #include <mutex>// mutex // version 2: // with problems below fixed: // 1. thread is safe now // 2. memory doesn't leak class Singleton{ public: typedef std::shared_ptr<Singleton> Ptr; ~Singleton(){ std::cout<<"destructor called!"<<std::endl; } Singleton(Singleton&)=delete; Singleton& operator=(const Singleton&)=delete; static Ptr get_instance(){ // "double checked lock" if(m_instance_ptr==nullptr){ std::lock_guard<std::mutex> lk(m_mutex); if(m_instance_ptr == nullptr){ m_instance_ptr = std::shared_ptr<Singleton>(new Singleton); } return m_instance_ptr; } } private: Singleton(){ std::cout<<"constructor called!"<<std::endl; } static Ptr m_instance_ptr; static std::mutex m_mutex; }; // initialization static variables out of class Singleton::Ptr Singleton::m_instance_ptr = nullptr; std::mutex Singleton::m_mutex; int main(){ Singleton::Ptr instance = Singleton::get_instance(); Singleton::Ptr instance2 = Singleton::get_instance(); return 0; }
執行結果如下,發現確實只構造了一次例項,並且發生了析構。
constructor called! destructor called!
shared_ptr和mutex都是C++11的標準,以上這種方法的優點是
- 基於 shared_ptr, 用了C++比較倡導的 RAII思想,用物件管理資源,當 shared_ptr 析構的時候,new 出來的物件也會被 delete掉。以此避免記憶體洩漏。
- 加了鎖,使用互斥量來達到執行緒安全。這裡使用了兩個 if判斷語句的技術稱為雙檢鎖 ;好處是,只有判斷指標為空的時候才加鎖,避免每次呼叫 get_instance的方法都加鎖,鎖的開銷畢竟還是有點大的。
不足之處在於: 使用智慧指標會要求使用者也得使用智慧指標,非必要不應該提出這種約束; 使用鎖也有開銷; 同時代碼量也增多了,實現上我們希望越簡單越好。
還有更加嚴重的問題,在某些平臺(與編譯器和指令集架構有關),==雙檢鎖會失效==!具體可以看這篇文章 ,解釋了為什麼會發生這樣的事情。
因此這裡還有第三種的基於 Magic Staic的方法達到執行緒安全
2.2.3 最推薦的懶漢式單例(magic static )——區域性靜態變數
#include <iostream> class Singleton { public: ~Singleton(){ std::cout<<"destructor called!"<<std::endl; } Singleton(const Singleton&)=delete; Singleton& operator=(const Singleton&)=delete; static Singleton& get_instance(){ static Singleton instance; return instance; } private: Singleton(){ std::cout<<"constructor called!"<<std::endl; } }; int main(int argc, char *argv[]) { Singleton& instance_1 = Singleton::get_instance(); Singleton& instance_2 = Singleton::get_instance(); return 0; }
執行結果
constructor called! destructor called!
這種方法又叫做 Meyers' SingletonMeyer's的單例 , 是著名的寫出《Effective C++》系列書籍的作者 Meyers 提出的。所用到的特性是在C++11標準中的Magic Static 特性:
If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.
如果當變數在初始化的時候,併發同時進入宣告語句,併發執行緒將會阻塞等待初始化結束。
這樣保證了併發執行緒在獲取靜態區域性變數的時候一定是初始化過的,所以具有執行緒安全性。
C++靜態變數的生存期 是從宣告到程式結束,這也是一種懶漢式。
這是最推薦的一種單例實現方式:
Single&
另外網上有人的實現返回指標而不是返回引用
static Singleton* get_instance(){ static Singleton instance; return &instance; }
這樣做並不好,理由主要是無法避免使用者使用delete instance
導致物件被提前銷燬。還是建議大家使用返回引用的方式。
2.2.4 函式返回引用
有人在網上提供了這樣一種單例的實現方式;
#include <iostream> class A { public: A() { std::cout<<"constructor" <<std::endl; } ~A(){ std::cout<<"destructor"<<std::endl; } }; A& ret_singleton(){ static A instance; return instance; } int main(int argc, char *argv[]) { A& instance_1 = ret_singleton(); A& instance_2 = ret_singleton(); return 0; }
嚴格來說,這不屬於單例了,因為類A只是個尋常的類,可以被定義出多個例項,但是亮點在於提供了ret_singleton
的方法,可以返回一個全域性(靜態)變數,起到類似單例的效果,這要求使用者必須保證想要獲取 全域性變數A ,只通過ret_singleton()的方法。
以上是各種方法實現單例的程式碼和說明,解釋了各種技術實現的初衷和原因。這裡會比較推薦 C++11 標準下的 2.2.3 的方式 ,即使用static local的方法 ,簡單的理由來說是因為其足夠簡單卻滿足所有需求和顧慮。
在某些情況下,我們系統中可能有多個單例,如果都按照這種方式的話,實際上是一種重複,有沒有什麼方法可以只實現一次單例而能夠複用其程式碼從而實現多個單例呢? 很自然的我們會考慮使用模板技術或者繼承的方法,
在我的部落格中有介紹過如何使用單例的模板。
2.3 單例的模板
2.3.1 CRTP 奇異遞迴模板模式實現
程式碼示例如下:
// brief: a singleton base class offering an easy way to create singleton #include <iostream> template<typename T> class Singleton{ public: static T& get_instance(){ static T instance; return instance; } virtual ~Singleton(){ std::cout<<"destructor called!"<<std::endl; } Singleton(const Singleton&)=delete; Singleton& operator =(const Singleton&)=delete; protected: Singleton(){ std::cout<<"constructor called!"<<std::endl; } }; /********************************************/ // Example: // 1.friend class declaration is requiered! // 2.constructor should be private class DerivedSingle:public Singleton<DerivedSingle>{ // !!!! attention!!! // needs to be friend in order to // access the private constructor/destructor friend class Singleton<DerivedSingle>; public: DerivedSingle(const DerivedSingle&)=delete; DerivedSingle& operator =(const DerivedSingle&)= delete; private: DerivedSingle()=default; }; int main(int argc, char* argv[]){ DerivedSingle& instance1 = DerivedSingle::get_instance(); DerivedSingle& instance2 = DerivedSingle::get_instance(); return 0; }
以上實現一個單例的模板基類,使用方法如例子所示意,子類需要將自己作為模板引數T
傳遞給Singleton<T>
模板; 同時需要將基類宣告為友元
,這樣才能呼叫子類的私有建構函式。
基類模板的實現要點是:
- 建構函式需要是protected ,這樣子類才能繼承;
- 使用了奇異遞迴模板模式 CRTP(Curiously recurring template pattern)
- get instance 方法和 2.2.3 的static local方法一個原理。
- 在這裡基類的解構函式可以不需要 virtual ,因為子類在應用中只會用 Derived 型別,保證了析構時和構造時的型別一致
2.3.2 不需要在子類宣告友元的實現方法
在stackoverflow
上, 有大神給出了不需要在子類中宣告友元的方法
,在這裡一併放出;精髓在於使用一個代理類 token,子類建構函式需要傳遞token類才能構造,但是把 token保護其起來, 然後子類的建構函式就可以是公有的了,這個子類只有Derived(token)
的這樣的建構函式,這樣使用者就無法自己定義一個類的例項了,起到控制其唯一性的作用。程式碼如下。
// brief: a singleton base class offering an easy way to create singleton #include <iostream> template<typename T> class Singleton{ public: static T& get_instance() noexcept(std::is_nothrow_constructible<T>::value){ static T instance{token()}; return instance; } virtual ~Singleton() =default; Singleton(const Singleton&)=delete; Singleton& operator =(const Singleton&)=delete; protected: struct token{}; // helper class Singleton() noexcept=default; }; /********************************************/ // Example: // constructor should be public because protected `token` control the access class DerivedSingle:public Singleton<DerivedSingle>{ public: DerivedSingle(token){ std::cout<<"destructor called!"<<std::endl; } ~DerivedSingle(){ std::cout<<"constructor called!"<<std::endl; } DerivedSingle(const DerivedSingle&)=delete; DerivedSingle& operator =(const DerivedSingle&)= delete; }; int main(int argc, char* argv[]){ DerivedSingle& instance1 = DerivedSingle::get_instance(); DerivedSingle& instance2 = DerivedSingle::get_instance(); return 0; }
2.3.3 函式模板返回引用
在 2.2.4 中提供了一種型別的全域性變數的方法,可以把一個一般的類,通過這種方式提供一個類似單例的
全域性性效果(但是不能阻止使用者自己宣告定義這樣的類的物件);在這裡我們把這個方法變成一個 template 模板函式,然後就可以得到任何一個類的全域性變數。
#include <iostream> class A { public: A() { std::cout<<"constructor" <<std::endl; } ~A(){ std::cout<<"destructor"<<std::endl; } }; template<typename T> T& get_global(){ static T instance; return instance; } int main(int argc, char *argv[]) { A& instance_1 = get_global<A>(); A& instance_2 = get_global<A>(); return 0; }
可以看到這種方式確實非常簡潔,同時類仍然具有一般類的特點而不受限制,當然也因此失去了單例那麼強的約束(禁止賦值、構造和拷貝構造)。
這裡把函式命名為get_global()
是為了強調,這裡可以通過這種方式獲取得到單例最重要的全域性變數特性;但是並不是單例的模式。
三、何時應該使用或者不使用單例
根據stackoverflow上的一個高票答案singleton-how-should-it-be-used :
You need to have one and only one object of a type in system
==你需要系統中只有唯一 一個例項存在的類的全域性 變數的時候才使用單例==。
-
如果使用單例,應該用什麼樣子的
How to create the best singleton:- The smaller, the better. I am a minimalist
- Make sure it is thread safe
- Make sure it is never null
- Make sure it is created only once
- Lazy or system initialization? Up to your requirements
- Sometimes the OS or the JVM creates singletons for you (e.g. in Java every class definition is a singleton)
- Provide a destructor or somehow figure out how to dispose resources
-
Use little memory
==越小越好,越簡單越好,執行緒安全,記憶體不洩露==
反對單例的理由
當然程式設計師是分流派的,有些是反對單例的,有些人是反對設計模式的,有些人甚至連面向物件都反對 :).
反對單例的理由有哪些:
參考文章
在本文寫作的過程中參考了一些部落格和stackoverflow 的回答,以超連結的方式體現在文中。另外還有一些我覺得非常精彩的回答,放在下面供讀者拓展閱讀
推薦閱讀: