C++ 單例模式總結與剖析

C++ 單例模式總結與剖析

單例多是最經常使用的簡單的一種設計模式,實現方法多樣,根據不一樣的需求有不一樣的寫法; 同時單例也有其侷限性,所以有不少人是反對使用單例的。本文對C++ 單例的常見寫法進行了一個總結, 包括懶漢式、線程安全、單例模板等; 按照從簡單到複雜,最終迴歸簡單的的方式按部就班地介紹,而且對各類實現方法的侷限進行了簡單的闡述,大量用到了C++ 11的特性如智能指針, magic static,線程鎖; 從頭至尾理解下來,對於學習和鞏固C++語言特性仍是頗有幫助的。本文的所有代碼在 g++ 5.4.0 編譯器下編譯運行經過,能夠在個人github 倉庫中找到。ios

1、什麼是單例

單例 Singleton 是設計模式的一種,其特色是隻提供惟一一個類的實例,具備全局變量的特色,在任何位置均可以經過接口獲取到那個惟一實例;
具體運用場景如:git

  1. 設備管理器,系統中可能有多個設備,可是隻有一個設備管理器,用於管理設備驅動;
  2. 數據池,用來緩存數據的數據結構,須要在一處寫,多處讀取或者多處寫,多處讀取;

2、C++單例的實現

2.1 基礎要點

  • 全局只有一個實例:static 特性,同時禁止用戶本身聲明並定義實例(把構造函數設爲 private)
  • 線程安全
  • 禁止賦值和拷貝
  • 用戶經過接口獲取實例:使用 static 類成員函數

2.2 C++ 實現單例的幾種方式

2.2.1 有缺陷的懶漢式

懶漢式(Lazy-Initialization)的方法是直到使用時才實例化對象,也就說直到調用get_instance() 方法的時候才 new 一個單例的對象, 若是不被調用就不會佔用內存。程序員

 1 #include <iostream>
 2 // version1:
 3 // with problems below:
 4 // 1. thread is not safe
 5 // 2. memory leak
 6 
 7 class Singleton{
 8 private:
 9     Singleton(){
10         std::cout<<"constructor called!"<<std::endl;
11     }
12     Singleton(Singleton&)=delete;
13     Singleton& operator=(const Singleton&)=delete;
14     static Singleton* m_instance_ptr;
15 public:
16     ~Singleton(){
17         std::cout<<"destructor called!"<<std::endl;
18     }
19     static Singleton* get_instance(){
20         if(m_instance_ptr==nullptr){
21               m_instance_ptr = new Singleton;
22         }
23         return m_instance_ptr;
24     }
25     void use() const { std::cout << "in use" << std::endl; }
26 };
27 
28 Singleton* Singleton::m_instance_ptr = nullptr;
29 
30 int main(){
31     Singleton* instance = Singleton::get_instance();
32     Singleton* instance_2 = Singleton::get_instance();
33     return 0;
34 }

運行的結果是github

constructor called!

能夠看到,獲取了兩次類的實例,卻只有一次類的構造函數被調用,代表只生成了惟一實例,這是個最基礎版本的單例實現,他有哪些問題呢?設計模式

  1. 線程安全的問題,當多線程獲取單例時有可能引起競態條件:第一個線程在if中判斷 m_instance_ptr是空的,因而開始實例化單例;同時第2個線程也嘗試獲取單例,這個時候判斷m_instance_ptr仍是空的,因而也開始實例化單例;這樣就會實例化出兩個對象,這就是線程安全問題的由來; 解決辦法:加鎖
  2. 內存泄漏. 注意到類中只負責new出對象,卻沒有負責delete對象,所以只有構造函數被調用,析構函數卻沒有被調用;所以會致使內存泄漏。解決辦法: 使用共享指針;

所以,這裏提供一個改進的,線程安全的、使用智能指針的實現;緩存

2.2.2 線程安全、內存安全的懶漢式單例 (智能指針,鎖)

 1 #include <iostream>
 2 #include <memory> // shared_ptr
 3 #include <mutex>  // mutex
 4 
 5 // version 2:
 6 // with problems below fixed:
 7 // 1. thread is safe now
 8 // 2. memory doesn't leak
 9 
10 class Singleton{
11 public:
12     typedef std::shared_ptr<Singleton> Ptr;
13     ~Singleton(){
14         std::cout<<"destructor called!"<<std::endl;
15     }
16     Singleton(Singleton&)=delete;
17     Singleton& operator=(const Singleton&)=delete;
18     static Ptr get_instance(){
19 
20         // "double checked lock"
21         if(m_instance_ptr==nullptr){
22             std::lock_guard<std::mutex> lk(m_mutex);
23             if(m_instance_ptr == nullptr){
24               m_instance_ptr = std::shared_ptr<Singleton>(new Singleton);
25             }
26         }
27         return m_instance_ptr;
28     }
29 
30 
31 private:
32     Singleton(){
33         std::cout<<"constructor called!"<<std::endl;
34     }
35     static Ptr m_instance_ptr;
36     static std::mutex m_mutex;
37 };
38 
39 // initialization static variables out of class
40 Singleton::Ptr Singleton::m_instance_ptr = nullptr;
41 std::mutex Singleton::m_mutex;
42 
43 int main(){
44     Singleton::Ptr instance = Singleton::get_instance();
45     Singleton::Ptr instance2 = Singleton::get_instance();
46     return 0;
47 }

運行結果以下,發現確實只構造了一次實例,而且發生了析構。安全

1 constructor called!
2 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 )——局部靜態變量

 1 #include <iostream>
 2 
 3 class Singleton
 4 {
 5 public:
 6     ~Singleton(){
 7         std::cout<<"destructor called!"<<std::endl;
 8     }
 9     Singleton(const Singleton&)=delete;
10     Singleton& operator=(const Singleton&)=delete;
11     static Singleton& get_instance(){
12         static Singleton instance;
13         return instance;
14 
15     }
16 private:
17     Singleton(){
18         std::cout<<"constructor called!"<<std::endl;
19     }
20 };
21 
22 int main(int argc, char *argv[])
23 {
24     Singleton& instance_1 = Singleton::get_instance();
25     Singleton& instance_2 = Singleton::get_instance();
26     return 0;
27 }

運行結果

1 constructor called!
2 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++靜態變量的生存期 是從聲明到程序結束,這也是一種懶漢式。

這是最推薦的一種單例實現方式:

  1. 經過局部靜態變量的特性保證了線程安全 (C++11, GCC > 4.3, VS2015支持該特性);
  2. 不須要使用共享指針,代碼簡潔;
  3. 注意在使用的時候須要聲明單例的引用 Single& 才能獲取對象。

另外網上有人的實現返回指針而不是返回引用

1 static Singleton* get_instance(){
2     static Singleton instance;
3     return &instance;
4 }

這樣作並很差,理由主要是沒法避免用戶使用delete instance致使對象被提早銷燬。仍是建議你們使用返回引用的方式。

2.2.4 函數返回引用

有人在網上提供了這樣一種單例的實現方式;

 1 #include <iostream>
 2 
 3 class A
 4 {
 5 public:
 6     A() {
 7         std::cout<<"constructor" <<std::endl;
 8     }
 9     ~A(){
10         std::cout<<"destructor"<<std::endl;
11     }
12 };
13 
14 
15 A& ret_singleton(){
16     static A instance;
17     return instance;
18 }
19 
20 int main(int argc, char *argv[])
21 {
22     A& instance_1 = ret_singleton();
23     A& instance_2 = ret_singleton();
24     return 0;
25 }

嚴格來講,這不屬於單例了,由於類A只是個尋常的類,能夠被定義出多個實例,可是亮點在於提供了ret_singleton的方法,能夠返回一個全局(靜態)變量,起到相似單例的效果,這要求用戶必須保證想要獲取 全局變量A ,只經過ret_singleton()的方法。

以上是各類方法實現單例的代碼和說明,解釋了各類技術實現的初衷和緣由。這裏會比較推薦 C++11 標準下的 2.2.3 的方式,即使用static local的方法,簡單的理由來講是由於其足夠簡單卻知足全部需求和顧慮。

在某些狀況下,咱們系統中可能有多個單例,若是都按照這種方式的話,其實是一種重複,有沒有什麼方法能夠只實現一次單例而可以複用其代碼從而實現多個單例呢? 很天然的咱們會考慮使用模板技術或者繼承的方法,
在個人博客中有介紹過如何使用單例的模板。

2.3 單例的模板

2.3.1 CRTP 奇異遞歸模板模式實現

代碼示例以下:

 1 // brief: a singleton base class offering an easy way to create singleton
 2 #include <iostream>
 3 
 4 template<typename T>
 5 class Singleton{
 6 public:
 7     static T& get_instance(){
 8         static T instance;
 9         return instance;
10     }
11     virtual ~Singleton(){
12         std::cout<<"destructor called!"<<std::endl;
13     }
14     Singleton(const Singleton&)=delete;
15     Singleton& operator =(const Singleton&)=delete;
16 protected:
17     Singleton(){
18         std::cout<<"constructor called!"<<std::endl;
19     }
20 
21 };
22 /********************************************/
23 // Example:
24 // 1.friend class declaration is requiered!
25 // 2.constructor should be private
26 
27 
28 class DerivedSingle:public Singleton<DerivedSingle>{
29    // !!!! attention!!!
30    // needs to be friend in order to
31    // access the private constructor/destructor
32    friend class Singleton<DerivedSingle>;
33 public:
34    DerivedSingle(const DerivedSingle&)=delete;
35    DerivedSingle& operator =(const DerivedSingle&)= delete;
36 private:
37    DerivedSingle()=default;
38 };
39 
40 int main(int argc, char* argv[]){
41     DerivedSingle& instance1 = DerivedSingle::get_instance();
42     DerivedSingle& instance2 = DerivedSingle::get_instance();
43     return 0;
44 }

以上實現一個單例的模板基類,使用方法如例子所示意,子類須要將本身做爲模板參數T 傳遞給 Singleton<T> 模板; 同時須要將基類聲明爲友元,這樣才能調用子類的私有構造函數。

基類模板的實現要點是:

  1. 構造函數須要是 protected,這樣子類才能繼承;
  2. 使用了奇異遞歸模板模式CRTP(Curiously recurring template pattern)
  3. get instance 方法和 2.2.3 的static local方法一個原理。
  4. 在這裏基類的析構函數能夠不須要 virtual ,由於子類在應用中只會用 Derived 類型,保證了析構時和構造時的類型一致

2.3.2 不須要在子類聲明友元的實現方法

在 stackoverflow上, 有大神給出了不須要在子類中聲明友元的方法,在這裏一併放出;精髓在於使用一個代理類 token,子類構造函數須要傳遞token類才能構造,可是把 token保護其起來, 而後子類的構造函數就能夠是公有的了,這個子類只有 Derived(token)的這樣的構造函數,這樣用戶就沒法本身定義一個類的實例了,起到控制其惟一性的做用。代碼以下。

 1 // brief: a singleton base class offering an easy way to create singleton
 2 #include <iostream>
 3 
 4 template<typename T>
 5 class Singleton{
 6 public:
 7     static T& get_instance() noexcept(std::is_nothrow_constructible<T>::value){
 8         static T instance{token()};
 9         return instance;
10     }
11     virtual ~Singleton() =default;
12     Singleton(const Singleton&)=delete;
13     Singleton& operator =(const Singleton&)=delete;
14 protected:
15     struct token{}; // helper class
16     Singleton() noexcept=default;
17 };
18 
19 
20 /********************************************/
21 // Example:
22 // constructor should be public because protected `token` control the access
23 
24 
25 class DerivedSingle:public Singleton<DerivedSingle>{
26 public:
27    DerivedSingle(token){
28        std::cout<<"destructor called!"<<std::endl;
29    }
30 
31    ~DerivedSingle(){
32        std::cout<<"constructor called!"<<std::endl;
33    }
34    DerivedSingle(const DerivedSingle&)=delete;
35    DerivedSingle& operator =(const DerivedSingle&)= delete;
36 };
37 
38 int main(int argc, char* argv[]){
39     DerivedSingle& instance1 = DerivedSingle::get_instance();
40     DerivedSingle& instance2 = DerivedSingle::get_instance();
41     return 0;
42 }

2.3.3 函數模板返回引用

在 2.2.4 中提供了一種類型的全局變量的方法,能夠把一個通常的類,經過這種方式提供一個相似單例的
全局性效果(可是不能阻止用戶本身聲明定義這樣的類的對象);在這裏咱們把這個方法變成一個 template 模板函數,而後就能夠獲得任何一個類的全局變量。

 1 #include <iostream>
 2 
 3 class A
 4 {
 5 public:
 6     A() {
 7         std::cout<<"constructor" <<std::endl;
 8     }
 9     ~A(){
10         std::cout<<"destructor"<<std::endl;
11     }
12 };
13 
14 template<typename T>
15 T& get_global(){
16     static T instance;
17     return instance;
18 }
19 
20 int main(int argc, char *argv[])
21 {
22     A& instance_1 = get_global<A>();
23     A& instance_2 = get_global<A>();
24     return 0;
25 }

能夠看到這種方式確實很是簡潔,同時類仍然具備通常類的特色而不受限制,固然也所以失去了單例那麼強的約束(禁止賦值、構造和拷貝構造)。
這裏把函數命名爲 get_global() 是爲了強調,這裏能夠經過這種方式獲取獲得單例最重要的全局變量特性;可是並非單例的模式。

3、什麼時候應該使用或者不使用單例

根據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
    越小越好,越簡單越好,線程安全,內存不泄露

反對單例的理由

固然程序員是分流派的,有些是反對單例的,有些人是反對設計模式的,有些人甚至連面向對象都反對 ????.

反對單例的理由有哪些:

相關文章
相關標籤/搜索