如何在 C++ 中實(shí)現(xiàn)一個單例類模板
單例模式是最簡單的設(shè)計(jì)模式之一。在實(shí)際工程中,如果一個類的對象重復(fù)持有資源的成本很高,且對外接口是線程安全的,我們往往傾向于將其以單例模式管理。
此篇我們在 C++ 中實(shí)現(xiàn)正確的單例模式。
選型
在 C++ 中,單例模式有兩種方案可選。
- 一是實(shí)現(xiàn)一個沒有可用的公開構(gòu)造函數(shù)的基類,并提供 GetInstance 之類的靜態(tài)接口,以便訪問子類唯一的對象。由于子類構(gòu)造必須調(diào)用基類構(gòu)造,但基類無公開構(gòu)造函數(shù)可用,這使得子類對象只能由基類及基類的友元來構(gòu)造,從而在機(jī)制上保證單例。
- 二是實(shí)現(xiàn)一個類模板,其模板參數(shù)是希望由單例管理的類的名字,并提供 GetInstance 之類的靜態(tài)接口。這種做法的好處是希望被單例管理的類,可以自由編寫,而無需繼承基類;并且在需要的時候,可以隨時脫去單例外衣。
此篇選擇實(shí)現(xiàn)一個單例類模板,其形如:
template <typename T> struct Singleton { static T* get(); T* operator->() const { return get(); } };
這里重載成員訪問運(yùn)算符,是為了可以實(shí)現(xiàn)這樣的簡寫 Singleton<T>()->func()
。
顯然,單例的實(shí)現(xiàn)核心在于靜態(tài)成員函數(shù) T* get()
。
一個典型的錯誤實(shí)現(xiàn)
一個典型的錯誤實(shí)現(xiàn),是使用所謂的雙重檢查(double check)。
#include <mutex> template <typename T> struct Singleton { static T* get() { static T* p{nullptr}; if (nullptr == p) { std::lock_guard<std::mutex> lock{mtx}; if (nullptr == p) { p = new T; } } return p; } T* operator->() const { return get(); } private: static std::mutex mtx; }; template <typename T> std::mutex Singleton<T>::mtx;
外層的檢查,是為了避免鎖住過大的區(qū)域,從而導(dǎo)致鎖的競爭特別頻繁;內(nèi)層的檢查,是為了確保只在別的線程沒有提前搶占鎖完成初始化工作而設(shè)計(jì)的。這種做法在 Java 下是正確的,但是在 C++ 下則沒有保證。
另外,值得一提的是,這里 p 的初始化的線程安全性,是由 C++ 標(biāo)準(zhǔn)保證的。——在 C++11 之后,標(biāo)準(zhǔn)保證函數(shù)靜態(tài)成員的初始化是線程安全的;對其讀寫則不保證線程安全。
使用標(biāo)準(zhǔn)庫提供的設(shè)施
在單例的實(shí)現(xiàn)中,我們實(shí)際上是希望實(shí)現(xiàn)「執(zhí)行且只執(zhí)行一次」的語義。C++11 之后,標(biāo)準(zhǔn)庫實(shí)際已經(jīng)提供了這樣的設(shè)施。其名為 std::once_flag
和 std::call_once
。它們內(nèi)部利用互斥量和條件變量組合,實(shí)現(xiàn)這樣的語義。值得一提的是,如果執(zhí)行過程中拋出異常,標(biāo)準(zhǔn)庫的設(shè)施不認(rèn)為這是一次「成功的執(zhí)行」。于是其他線程可以繼續(xù)搶占鎖來執(zhí)行函數(shù)。
我們利用標(biāo)準(zhǔn)庫設(shè)施來實(shí)現(xiàn)這個類模板。
#include <mutex> template <typename T> struct Singleton { static T* get() { static T* p{nullptr}; std::call_once(flag, [&]() -> void { p = new T; }); return p; } T* operator->() const { return get(); } private: static std::once_flag flag; }; template <typename T> std::once_flag Singleton<T>::flag;
于是你可以寫出類似這樣的代碼:
#include <mutex> #include <iostream> #include <future> #include <vector> #include "singleton.h" struct Foo { void address() const { std::lock_guard<std::mutex> lock{mtx}; std::cout << static_cast<void*>(const_cast<Foo*>(this)) << '\n'; } mutable std::mutex mtx; }; int main() { Singleton<Foo>()->address(); std::vector<std::future<void>> futs; for (size_t i = 0; i != 10; ++i) { futs.emplace_back(std::async(&Foo::address, Singleton<Foo>::get())); } for (auto& fut : futs) { fut.get(); } return 0; }
得到的輸出類似這樣:
$ ./a.out 0x7fbc6f405a10 0x7fbc6f405a10 0x7fbc6f405a10 0x7fbc6f405a10 0x7fbc6f405a10 0x7fbc6f405a10 0x7fbc6f405a10 0x7fbc6f405a10 0x7fbc6f405a10 0x7fbc6f405a10 0x7fbc6f405a10
Bonus:需要注意的是,所有的 std::once_flag
內(nèi)部共享了同一對互斥量和條件變量。因此當(dāng)存在很多 std::call_once
的時候,性能會有所下降。這一點(diǎn)可能需要注意一下。不過,如果存在很多 std::call_once
,大概也說明程序設(shè)計(jì)不合理吧……
Bonus:注意我們這里沒有釋放 p 指向的對象。這是因?yàn)?C++ 程序?qū)o態(tài)變量的析構(gòu)順序是不確定的。如果靜態(tài)變量之間有相互依賴,析構(gòu)被依賴的對象可能會導(dǎo)致段錯誤。因此干脆就不釋放了,這是所謂的 LeakySingleton
。當(dāng)然,如果你的工程當(dāng)中有實(shí)現(xiàn)一個通用的 ExitManager
,是有可能正確析構(gòu)的。但考慮到還可能大量使用第三方庫,而第三方庫不可能使用你實(shí)現(xiàn)的 ExitManager
,于是管理所有靜態(tài)變量的析構(gòu)又變得不可能,于是干脆就不管它了。
如此如此,這般這般
如果你仔細(xì)讀了這篇文章,你可能會忽然意識到剛才看到了這句話:「在 C++11 之后,標(biāo)準(zhǔn)保證函數(shù)靜態(tài)成員的初始化是線程安全的;對其讀寫則不保證線程安全?!?/p>
既然如此,我們?yōu)樯哆€要費(fèi)勁使用 std::once_flag
和 std::call_once
呢?直接利用 static
hack 出一個單例類模板不就好了嗎?
template <typename T> struct Singleton { static T* get() { static T ins; return &ins; } T* operator->() const { return get(); } };
以上就是如何在 C++ 中實(shí)現(xiàn)一個單例類模板的詳細(xì)內(nèi)容,更多關(guān)于c++ 單例類模板的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
判斷指定的進(jìn)程或程序是否存在方法小結(jié)(vc等)
VC判斷進(jìn)程是否存在?比如我想知道記事本是否運(yùn)行,要用到哪些函數(shù)等實(shí)例,需要的朋友可以參考下2013-01-01C語言采用文本方式和二進(jìn)制方式打開文件的區(qū)別分析
這篇文章主要介紹了C語言采用文本方式和二進(jìn)制方式打開文件的區(qū)別分析,有助于讀者更好的理解文本文件與二進(jìn)制文件的原理,需要的朋友可以參考下2014-07-07C語言實(shí)現(xiàn)小型工資管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C語言實(shí)現(xiàn)小型工資管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-02-02C語言光標(biāo)旋轉(zhuǎn)與倒計(jì)時功能實(shí)現(xiàn)示例詳解
這篇文章主要為大家介紹了C語言實(shí)現(xiàn)光標(biāo)旋轉(zhuǎn)與倒計(jì)時功能的示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2021-11-11C語言實(shí)現(xiàn)輸入ascii碼,輸出對應(yīng)的字符方式
這篇文章主要介紹了C語言實(shí)現(xiàn)輸入ascii碼,輸出對應(yīng)的字符方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-01-01