奇異遞歸模板模式是C++模板編程時的一種慣用法(idiom):把派生類作為基類的模板參數。更一般地被稱作F-bound polymorphism,是一類F 界量化,相關介紹可以參考 wiki 奇異遞歸模板模式。
基本介紹
- 中文名:奇異遞歸模板模式
- 外文名:curiously recurring template pattern
- 縮寫:CRTP
簡介,一般形式,CRTP的特點,CRTP基本範式,易錯點,靜態多態,例子,對象計數,多態複製構造,不可派生的類,std::enable_shared_from_this,CRTP特點總結,
簡介
一般形式
template<class T>class Base{ // methods within Base can use template to access members of Derived};class Derived : public Base<Derived>{ // ...};
CRTP的特點
- 繼承自模板類;
2. 使用派生類作為模板參數特化基類;
CRTP基本範式
CRTP如下的代碼樣式:
template <typename T>class Base{ ...}; // use the derived class itself as a template parameter of the base classclass Derived : public Base<Derived>{ ...};
這樣做的目的是在基類中使用派生類,從基類的角度來看,派生類其實也是基類,通過向下轉換[downcast],因此,基類可以通過static_cast把其轉換到派生類,從而使用派生類的成員,形式如下:
template <typename T>class Base{public: void doWhat() { T& derived = static_cast<T&>(*this); // use derived... }};
注意 這裡不使用dynamic_cast,因為dynamic_cast一般是為了確保在運行期(run-time)向上向下轉換的正確性。CRTP的設計是:派生類就是基類的模板參數,因此static_cast足矣。
易錯點
當兩個類繼承自同一個CRTP base類時,如下代碼所示,會出現錯誤(Derived2派生的基類模板參數不是Derived2)。
class Derived1 : public Base<Derived1>{ ...};class Derived2 : public Base<Derived1> // bug in this line of code{ ...};
為了防止種錯誤的出現,可以寫成如下的代碼形式:
template <typename T>class Base{public: // ...private:// import Base(){}; friend T;};
通過代碼可以看出來,基類中添加一個私有構造函式,並且模板參數T是Base的友元。這樣做可行是因為,派生類需要調用基類的構造函式(編譯器會默認調用的),由於Base的構造函式是私有的(private),除了友元沒有其他類可以訪問的,而且基類獨立的友元是其實例化模板參數。
派生類會隱藏和基類同名的方法,如下代碼所示:
template <typename T>class Base{public: void do();};class Derived : public Base<Derived>{public: void do(); // oops this hides the doSomething methods from the base class !}
出現這個情況的緣由是,以作用域為基礎的“名稱遮掩規則”,從名稱查找的觀點來看,如果實現了Derived::do(), 則Base::do不再被Derived繼承。
靜態多態
Andrei Alexandrescu在Modern C++ Design中稱之為靜態多態(static polymorphism)。
template <class T> struct Base{ void interface() { // ... static_cast<T*>(this)->implementation(); // ... } static void static_func() { // ... T::static_sub_func(); // ... }};struct Derived : Base<Derived>{ void implementation(); static void static_sub_func();};
基類模板利用了其成員函式體(即成員函式的實現)將不被實例化直至聲明很久之後(實際上只有被調用的模板類的成員函式才會被實例化);並利用了派生類的成員,這是通過{{ilh|lang={{langname|Type conversion}}|lang-code=Type conversion|1=類型轉化|2=類型轉化|d=|nocat=}}。
在上例中,Base<Derived>::interface(),雖然是在struct Derived之前就被聲明了,但未被編譯器實例化直至它被實際調用,這發生於Derived聲明之後,此時Derived::implementation()的聲明是已知的。
這種技術獲得了類似於虛函式的效果,並避免了動態多態的代價。也有人把CRTP稱為“模擬的動態綁定”。
考慮一個基類,沒有虛函式,則它的成員函式能夠調用的其它成員函式,只能是屬於該基類自身。當從這個基類派生其它類時,派生類繼承了所有未被覆蓋(overridden)的基類的數據成員與成員函式。如果派生類調用了一個被繼承的基類的函式,而該函式又調用了其它成員函式,這些成員函式不可能是派生類中的派生或者覆蓋的成員函式。也就是說,基類中是看不到派生類的。但是,基類如果使用了CRTP,則在編譯時派生類的覆蓋的函式可被選中調用。這效果相當於編譯時模擬了虛函式調用但避免了虛函式的尺寸與調用開銷(VTBL結構與方法查找、多繼承機制)等代價。但CRTP的缺點是不能在運行時做出動態綁定。
不通過虛函式機制,基類訪問派生類的私有或保護成員,需要把基類聲明為派生類的友元(friend)。如果一個類有多個基類都出現這種需求,聲明多個基類都是友元會很麻煩。一種解決技巧是在派生類之上再派生一個accessor類,顯然accessor類有權訪問派生類的保護函式;如果基類有權訪問accessor類,就可以間接調用派生類的保護成員了。這種方法被boost的多個庫使用,如:Boost.Python中的def_visitor_access和Boost.Iterator的iterator_core_access。原理示例代碼如下:
template<class DerivedT> class Base{ private: struct accessor : DerivedT { // accessor類沒有數據成員,只有一些靜態成員函式 static int foo(DerivedT& derived) { int (DerivedT::*fn)() = &DeriveT::do_foo; //獲取DerivedT::do_foo的成員函式指針 return (derived.*fn)(); // 通過成員函式指針的函式調用 } }; // accessor類僅是Base類的成員類型,而沒有實例化為Base類的數據成員。 public: DerivedT& derived() // 該成員函式返回派生類的實例的引用 { return static_cast<DerivedT&>(*this); } int foo() { // 該函式具體實現了業務功能 return accessor::foo( this->derived()); }}; struct Derived : Base<Derived> // 派生類不需要任何特別的友元聲明 protected: int do_foo() { // ... 具體實現 return 1; }};
例子
對象計數
統計一個類的實例對象創建與析構的數據。This can be easily solved using CRTP:
template <typename T>struct counter{ static int objects_created; static int objects_alive; counter() { ++objects_created; ++objects_alive; } counter(const counter&) { ++objects_created; ++objects_alive; }protected: ~counter() // objects should never be removed through pointers of this type { --objects_alive; }};template <typename T> int counter<T>::objects_created( 0 );template <typename T> int counter<T>::objects_alive( 0 );class X : counter<X>{ // ...};class Y : counter<Y>{ // ...};
多態複製構造
當使用多態時,常需要基於基類指針創建對象的一份拷貝。常見辦法是增加clone虛函式在每一個派生類中。使用CRTP,可以避免在派生類中增加這樣的虛函式。
// Base class has a pure virtual function for cloningclass Shape {public: virtual ~Shape() {} virtual Shape *clone() const = 0;};// This CRTP class implements clone() for Derivedtemplate <typename Derived>class Shape_CRTP : public Shape {public: virtual Shape *clone() const { return new Derived(static_cast<Derived const&>(*this)); }};// Nice macro which ensures correct CRTP usage#define Derive_Shape_CRTP(Type) class Type: public Shape_CRTP<Type>// Every derived class inherits from Shape_CRTP instead of ShapeDerive_Shape_CRTP(Square) {};Derive_Shape_CRTP(Circle) {};
不可派生的類
template<typename T> class MakeFinally{ private: MakeFinally(){};//只有MakeFinally的友類才可以構造MakeFinally ~MakeFinally(){}; friend T;};class MyClass:public virtual MakeFinally<MyClass>{};//MyClass是不可派生類//由於虛繼承,所以D要直接負責構造MakeFinally類,從而導致編譯報錯,所以D作為派生類是不合法的。class D: public MyClass{};//另外,如果D類沒有實例化對象,即沒有被使用,實際上D類是被編譯器忽略掉而不報錯int main(){MyClass var1;// D var2; //這一行編譯將導致錯誤,因為D類的默認構造函式不合法}
std::enable_shared_from_this
class mySharedClass:public std::enable_shared_from_this<mySharedClass>{public: // ...};int main(){ std::vector(std::shared_ptr<mySharedClass>) spv; spv.push_back(new mySharedClass()); std::shared_ptr<mySharedClass> p(new mySharedClass()); mySharedClass &c=*p; spv.emplace_back(c.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)。