本文探討一下智能指針和GOF設計模式的關系,如果按照設計模式的背后思想來分析,可以發現圍繞智能指針的設計和實現有設計模式的一些思想體現。當然,它們也不是嚴格意義上面向對象的設計模式,畢竟它們沒有那么分明的類層次體系,和GOF經典設計模式在外在形式上有所差別,重點是理解設計模式的思想在它們身上的體現,以及怎樣幫助它們實現意圖的。
限于篇幅,分成了幾篇文章來介紹,先從對象的創建開始。
1、工廠模式
工廠模式是把創建對象和使用對象的職責分離了,它們可以控制對象的創建過程,封裝了創建細節,讓用戶不再關心具體對象的創建過程。
C++標準庫提供了一些輔助函數和輔助類來創建智能指針對象,它們都是工廠方法或者工廠類。創建unique_ptr和shared_ptr除了使用常規的構造函數之外,還提供了三個工廠方法:make_shared()、make_unique()和allocate_shared(),它們都是簡單工廠方法;此外,為了能夠從一個shared_ptr對象的內部,通過this指針創建一個shared_ptr對象,工廠類enable_shared_from_this<T>還提供了成員函數shared_from_this()作為工廠方法。
那么,使用工廠模式創建智能指針對象有什么好處呢?
首先,通過工廠方法可以給創建過程起一個富有表達力、自解釋的名稱,能有效地幫助程序員容易使用,甚至無需提供閱讀文檔接口,而類的構造函數必須和類名完全一樣,無法能夠見文知意。make_shared()和make_unique(),一看就知道是創建shared_ptr和unique_ptr對象,更具特色的是shared_from_this(),通過它的名稱就應該知道這個函數是通過this指針來創建shared_ptr對象,既然有this字眼,也能知道應該是在一個類的內部使用。
其次,關注點分離,分離了創建智能指針和使用智能指針的職責,讓程序員不再關心智能指針對象的創建過程,不再關心是如何創建出來的,程序員把關注點放在智能指針的使用上面,減輕了程序員的心智負擔。有人說,C++有了智能指針之后,就不應該在程序中出現new和delete了,可能說的絕對了點,但顯然工廠方法make_shared()和make_unique()給了他這樣說的底氣。
再者,工廠模式可以控制智能指針對象的創建過程,這也是核心意圖,它的作用有下面幾點:
1、保證在堆中創建資源對象
智能指針缺省要求管理的資源對象是在堆中創建的對象,如果把一個指向棧上創建的對象的指針,讓unique_ptr或shared_ptr去管理,最后在析構時會發生異常,顯然是不對的。如何避免程序員無意中犯這樣錯誤?那么作為控制對象創建過程的工廠模式,用在這兒再合適不過了。由工廠方法來控制智能指針對象的創建過程,程序員在make_unique和make_shared工廠函數中只傳遞創建資源對象相關參數就行了,即保證智能指針管理的肯定是使用new操作符創建的對象,這樣就保證了程序的安全性。
2、保證創建過程中資源對象不泄露
make_unique()和make_shared()能夠保證資源對象的釋放安全,我們稍加留意就會發現,無論是shared_ptr類還是unique_ptr類,在它們的構造函數中都沒有同時初始化對象資源,對象資源是在外部使用new操作符在堆上創建之后,以裸指針的形式作為構造函數的參數來創建智能指針對象,也就是說資源對象的創建和智能指針對象的創建,它們不是一體的,它們之間是有空隙的,如果在這個間隙中有別的代碼運行,并發生了異常,可能會造成資源泄漏。比如(這個例子來自Effective Modern C++ 條款21):
processWidget(std::shared_ptr<Widget>(new Widget), computePriority());
因為編譯器在編譯時,可能是按照下面的順序生成代碼:1、實施“new Widget”,2、執行computePriority(),3、運行std::shared_ptr構造函數。如果生成了這樣的代碼,并且在運行時computePriority()發生了異常,那么在第1步動態分配的Widget對象會被泄露,因為它沒有機會被存儲到第3步才接管它的shared_ptr對象中去。如果使用make_shared()工廠方法來創建shared_ptr對象,就不會有潛在的資源泄露風險了:
processWidget(make_shared<Widget>(), computePriority());
使用make_unique和make_shared讓unique_ptr和shared_ptr在創建對象時就同時獲取了資源,即獲取資源即初始化(符合了RAII慣例的字面意思)。
3、創建時優化內存空間布局
工廠方法make_shared()在創建shared_ptr對象時,還可以對內存布局進行優化。shared_ptr對象包含了兩個指針成員,一個指向資源對象,一個指向控制塊,需要進行兩次new操作才能初始化完,在訪問時需要分別進行兩次指針解引用。如果資源對象和控制塊分配在同一個內存塊中,這樣就有更好的空間局部性,對cache更友好。在make_shared()內部可以進行控制這個實現過程,把控制塊的大小與資源的大小的和作為分配內存空間的大小,new一次就行了,然后分別讓資源對象指針和控制塊指針分別指向它們所在的位置,并初始化。讓內存空間更緊湊,節省了內存空間,同時因為有更好的cache局部性,也提高了訪問速度。
4、保證安全創建智能指針對象
weak_ptr類的lock()成員函數也是創建shared_ptr對象的一個工廠方法,控制的是從一個還沒有銷毀資源對象的shared_ptr對象中創建另一個shared_ptr對象。在多線程環境下,增加shared_ptr對象的引用計數和把控制塊指針、資源對象指針作為參數創建shared_ptr對象時,它們不是原子操作,在多線程下會存在data race。因此,在創建時需要保證線程安全,顯然交給程序員在外面實現是不現實的,那就把它封裝在一個工廠方法中,讓它來控制shared_ptr的創建過程,保證創建過程的線程安全。
enable_shared_from_this<T>類的成員函數shared_from_this()也是創建shared_ptr對象的一個工廠方法,控制的是通過資源對象的this指針來創建shared_ptr對象。它保證了是從一個已有的shared_ptr對象中創建的,如果不是,則會拋出異常;同時也保證了不會發生同一個this指針被多個不同shared_ptr對象管理生存期的錯誤,否則,如果用戶在外部隨便把this指針作為參數去調用shared_ptr構造函數,可能是重復管理,而發生錯誤。
最后,智能指針是裸指針的包裝類,而裸指針又指向資源對象,控制創建智能指針對象的過程,實際上也是在控制創建資源對象的過程。例如,工廠模式在控制智能指針對象的創建過程同時,也控制了資源對象使用new在堆上創建,如make_unique()和make_shared(),也控制了shared_ptr對象從this指針創建的過程,如shared_from_this()。
工廠模式控制了智能指針和資源對象的創建過程,那么銷毀工作又是如何實現的呢?下一篇文章繼續介紹。