奇異遞歸模板模式

奇異遞歸模板模式

奇異遞歸模板模式是C++模板編程時的一種慣用法(idiom):把派生類作為基類的模板參數。更一般地被稱作F-bound polymorphism,是一類F 界量化,相關介紹可以參考 wiki 奇異遞歸模板模式。

簡介

一般形式

CRTP的特點

繼承自模板類;

1.

繼承自模板類;

2. 使用派生類作為模板參數特化基類;

CRTP基本範式

CRTP如下的代碼樣式:

這樣做的目的是在基類中使用派生類,從基類的角度來看,派生類其實也是基類,通過向下轉換[downcast],因此,基類可以通過static_cast把其轉換到派生類,從而使用派生類的成員,形式如下:

注意 這裡不使用dynamic_cast,因為dynamic_cast一般是為了確保在運行期(run-time)向上向下轉換的正確性。CRTP的設計是:派生類就是基類的模板參數,因此static_cast足矣。

易錯點

當兩個類繼承自同一個CRTP base類時,如下代碼所示,會出現錯誤(Derived2派生的基類模板參數不是Derived2)。

為了防止種錯誤的出現,可以寫成如下的代碼形式:

通過代碼可以看出來,基類中添加一個私有構造函式,並且模板參數T是Base的友元。這樣做可行是因為,派生類需要調用基類的構造函式(編譯器會默認調用的),由於Base的構造函式是私有的(private),除了友元沒有其他類可以訪問的,而且基類獨立的友元是其實例化模板參數。

派生類會隱藏和基類同名的方法,如下代碼所示:

出現這個情況的緣由是,以作用域為基礎的“名稱遮掩規則”,從名稱查找的觀點來看,如果實現了Derived::do(), 則Base::do不再被Derived繼承。

靜態多態

Andrei Alexandrescu在Modern C++ Design中稱之為 靜態多態(static polymorphism)。

基類模板利用了其成員函式體(即成員函式的實現)將不被實例化直至聲明很久之後(實際上只有被調用的模板類的成員函式才會被實例化);並利用了派生類的成員,這是通過{{ilh|lang={{langname|Type conversion}}|lang-code=Type conversion|1=類型轉化|2=類型轉化|d=|nocat=}}。

在上例中,Base<Derived>::interface(),雖然是在struct Derived之前就被聲明了,但未被編譯器實例化直至它被實際調用,這發生於Derived聲明之後,此時Derived::implementation()的聲明是已知的。

這種技術獲得了類似於虛函式的效果,並避免了動態多態的代價。也有人把CRTP稱為“模擬的動態綁定”。

這種模式廣泛用於WindowsATL與WTL庫,以及Boost.Iterator,Boost.Python或者Boost.Serialization等庫中。

考慮一個基類,沒有虛函式,則它的成員函式能夠調用的其它成員函式,只能是屬於該基類自身。當從這個基類派生其它類時,派生類繼承了所有未被覆蓋(overridden)的基類的數據成員與成員函式。如果派生類調用了一個被繼承的基類的函式,而該函式又調用了其它成員函式,這些成員函式不可能是派生類中的派生或者覆蓋的成員函式。也就是說,基類中是看不到派生類的。但是,基類如果使用了CRTP,則在編譯時派生類的覆蓋的函式可被選中調用。這效果相當於編譯時模擬了虛函式調用但避免了虛函式的尺寸與調用開銷(VTBL結構與方法查找、多繼承機制)等代價。但CRTP的缺點是不能在運行時做出動態綁定。

不通過虛函式機制,基類訪問派生類的私有或保護成員,需要把基類聲明為派生類的友元(friend)。如果一個類有多個基類都出現這種需求,聲明多個基類都是友元會很麻煩。一種解決技巧是在派生類之上再派生一個accessor類,顯然accessor類有權訪問派生類的保護函式;如果基類有權訪問accessor類,就可以間接調用派生類的保護成員了。這種方法被boost的多個庫使用,如:Boost.Python中的def_visitor_access和Boost.Iterator的iterator_core_access。原理示例代碼如下:

例子

對象計數

統計一個類的實例對象創建與析構的數據。This can be easily solved using CRTP:

多態複製構造

當使用多態時,常需要基於基類指針創建對象的一份拷貝。常見辦法是增加clone虛函式在每一個派生類中。使用CRTP,可以避免在派生類中增加這樣的虛函式。

不可派生的類

std::enable_shared_from_this

CRTP特點總結

CRTP是一種靜態多態(static polymorphism/Static binding/Compile-Time binding)與其對應的是動態多態(dynamic polymorphism/Dynamic binding/Run-Time binding)。靜態多態與和動態的區別是:多態是動態綁定(運行時綁定 run-time binding),CRTP是靜態綁定(編譯時綁定 compile-time binding)。其中,動態多態在實現多態時,需要重寫虛函式,這種運行時綁定的操作往往需要查找虛表等,效率低。另,template的核心技術在於編譯期多態機制,與運行期多態(runtime polymorphism)相比,這種動態機制提供想編譯期多態性,給了程式運行期無可比擬的效率優勢。因此,如果想在編譯期確定通過基類來得到派生類的行為,CRTP便是一種絕佳的選擇。

AD(automatic differentiation,自動微分),相關autodiff工具中也有相當一部分這種庫在使用CRTP技術,這是由於在數值計算中,對於不同的模型會使用不同的方法,一般使用繼承提供統一接口,但又希望不損失效率,因此,此時便可利用CRTP了,子類的operator(expression)實現將覆蓋基類的operator實現,並可以編譯期靜態綁定至子類的方法,AD自動求導效率堪比手動寫出相關程式(所謂的 Hand coded)。

相關詞條

熱門詞條

聯絡我們