前面的文章介紹了使用工廠模式來封裝智能指針對象的創建過程,下面介紹一下工廠類
enable_shared_from_this的實現方案。
4、模板方法模式
在前面的文章分析過,enable_shared_from_this<T>類是一個工廠基類,提供的工廠方法是shared_from_this(),而shared_ptr<T>就是一個具體的產品類,每一個shared_ptr<T>對象都需要一個具體的工廠類來創建它,這個具體的工廠類就是T,它是enable_shared_from_this<T>的派生類,同時又是模板參數類型。那么,C++標準庫是怎么通過this指針來創建shared_ptr<T>對象的呢?
按照常規的做法,可以繼承工廠基類然后由派生類來實現基類定義的工廠方法接口,然而當在一個對象內部通過this指針創建一個shared_ptr對象時,卻不是一件容易的事情:
首先,要判斷資源對象是否是在堆中,如果資源對象不是在堆中,那么使用shared_ptr<T>(this)創建的智能指針對象,在析構時就會發生異常。
其次,要確定資源對象是否已經被shared_ptr管理了,如果管理了,也不能直接使用shared_ptr<T>(this)創建的智能指針對象,否則會導致由兩個不同的shared_ptr來管理this,最后會發生重復釋放內存的錯誤。
再次,如果在資源對象內部使用shared_ptr<T>(this)創建了智能指針對象,但是用戶在外部是不知道的,可能又使用shared_ptr<T>(obj_ptr)創建了智能指針對象,也會發生重復釋放內存的錯誤。
如果這個創建過程,完全交給程序員來實現,需要仔細地約束各方面的代碼實現,還得需要相關人員認真地評審,防止出現上面描述的錯誤,帶來了極大的風險和不便。C++標準庫的思路是交由代碼來解決,把通過this指針創建shared_ptr對象的代碼封裝起來形成固定的模板套路,只把需要定制的部分交給派生類去實現,也就是模板方法模式,即在模板基類中把通過this指針創建shared_ptr對象的過程封裝成“模板方法”,它執行過程中要使用具體派生類提供的“鉤子函數",這樣哪個類要想提供shared_from_this()功能,就繼承模板基類并實現”鉤子函數“就可以了。下面是GOF模板方法模式的結構圖:
從結構圖中可以看出,模板方法模式實際上就是面向對象編程中的繼承和動態多態機制,也就是在基類中定義virtual函數接口,也就是“鉤子方法”,在派生類中實現virtual函數,從而讓基類中的“模板方法”能夠通過動態綁定的方式,來調用派生類提供的virtual函數。
不過enable_shared_from_this<T>作為模板基類與上面的模板方法模式在形式上不同,它沒有采用面向對象編程的多態機制,而是利使用了一種特殊的繼承方式實現的,即CRTP。CRTP采用了C++的模板技術,雖然也是繼承方式,但卻是在基類中通過把自己強制轉為派生類類型的方式來調用派生類實現的函數,這是二者最大的區別。模板方法模式是一個共有的基類,然后有多個同一族類的派生類,它們復用了基類中的“模板方法”,并重寫了virtual函數,屬于動態綁定;而CRTP方式是一個基類僅有一個派生類,這樣完全可以把基類強制轉換成派生類,調用的是派生類的非virtual成員函數,屬于靜態綁定機制。同樣,因為CRTP派生類繼承了基類,它復用了基類的“模板方法”,因此,外界也可以調用派生類的“模板方法”來實現具體的功能。
shared_from_this()創建shared_ptr對象的套路是,定義了一個weak_ptr數據成員,在創建shared_ptr對象時,就使用這個weak_ptr成員來創建,派生類顯然是無法設置這個weak_ptr成員的,也就是說無法設計成提供一個“鉤子函數”的形式,而weak_ptr類型和shared_ptr類型息息相關,因此enable_shared_from_this<T>就讓shared_ptr<T>作為它的友元類,要求shared_ptr在創建對象時,同時初始化這個weak_ptr成員。這樣,如果派生類T沒有使用shared_ptr對象來管理自己的生存期,也就初始化不了這個weak_ptr成員,進而也就無法通過this創建出shared_ptr對象,而通過weak_ptr對象創建的shared_ptr對象和原來的shared_ptr對象共享T資源對象的所有權,也不會發生直接使用this指針創建了shared_ptr對象之后,導致由兩個不同的shared_ptr來管理this的錯誤。
可見,在這個模板方法模式中,“鉤子方法”不是一個具體函數,它是一個抽象的概念,或者說是一個“鉤子數據”,在這里就是它的weak_ptr類型的數據成員,模板方法在實現套路化的邏輯時,需要使用weak_ptr對象數據成員來創建shared_ptr對象,這個weak_ptr對象是在創建shared_ptr對象時初始化的,因此要求創建它的派生類對象時,必須要被一個shared_ptr對象托管,這樣shared_ptr在創建對象時,就會同時初始化這個weak_ptr成員。也就是說,派生類不需要提供具體的“鉤子函數”,只要它在創建時候,同時創建一個shared_ptr對象來管理它的生存期,它自然而然地就為基類的“模板方法”提供了“鉤子數據”。
這里,相當于把通過this指針創建shared_ptr對象的過程給封裝成模板方法,誰要是想通過this指針創建shared_ptr對象就可以直接繼承enable_shared_from_this類,它的模板方法自己實現了這個功能,派生類無需專門編寫代碼,為程序員編程帶來了便利。下面是結構圖:
enable_shared_from_this<resource>是工廠基類也是模板方法類,它的成員函數shared_from_this()是"模板方法",職責是使用資源對象resource的this指針來創建一個shared_ptr對象;資源對象resource類是它的一個公共派生類,是工廠派生類也是模板方法派生類,具體產品對象shared_ptr<resource>就是從resource中創建出來的,也就是一個產品對象通過“模板方法”創建了一個管理自己生存期的shared_ptr對象。示例代碼如下:
class resource : public enable_shared_from_this<resource> {data_obj obj;public:void process() {auto sp = shared_from_this();...}... // 其它成員函數
};
...
只要繼承了enable_shared_from_this<T>類,它就是一個模板方法派生類,就可以使用模板方法類的shared_from_this(),然而這只是“模板方法”函數,還得需要派生類自己提供“鉤子方法”函數,也就是保證派生類創建的對象被shared_ptr托管,這樣就可以根據模板方法來通過this指針創建shared_ptr的副本了。因此,resoure資源對象可以通過:
shared_ptr<resource> res1(new resource);
shared_ptr<resource> res2 = make_shared<resource>();
shared_ptr<resource> res3 = weak_ptr_obj.lock();
shared_ptr<resource> res4(weak_ptr_obj());
shared_ptr<resource> res5(unique_ptr_obj());
等這些方式來創建shared_ptr<resource>對象,可以把這些創建方式等同于“鉤子方法”,派生類resource只要能夠創建出shared_ptr<resource>對象,當在resource對象內部使用this指針調用“模板方法”shared_shared_this()時,就會按照固定的套路創建出一個shared_ptr<resource>對象。顯然這樣就簡化了根據this創建shared_ptr<resource>對象的過程,程序也不需要作太多的考慮,只要保證resource對象是使用shared_ptr智能指針管理的就可以了。
總之,工廠方法shared_from_this()是使用模板方法模式實現的,而模板模式又是使用CRTP慣用法來實現的。
參考:
https://cloud.tencent.com/developer/article/2362395