1. 程式人生 > >C++多型:深入CRTP,理解編譯期的多型

C++多型:深入CRTP,理解編譯期的多型

虛擬函式帶來的額外CPU消耗

 考慮如下的程式碼:

class D {
public:
    int num;
    D(int i = 0) { num = i; }
    virtual void print() { cout << "I'm a D. my num=" << num << endl; }


};
class E :public D {
public:
    E(int i = 0) { num = i; }
    void print() { cout << "I'm a E. my num=" << num << endl; }
    void
not_virtual_print() { cout << "not virtual func" << num << endl; } }; int main() { E* e = new E(1); e->print(); e->not_virtual_print(); delete e; return 0; }

注意如下的虛擬函式呼叫和普通成員函式呼叫的彙編程式碼:

    e->print();
008C2788  mov         eax,dword ptr [e]  
008C278B  mov
edx,dword ptr [eax] 008C278D mov esi,esp 008C278F mov ecx,dword ptr [e] 008C2792 mov eax,dword ptr [edx] 008C2794 call eax 008C2796 cmp esi,esp 008C2798 call __RTC_CheckEsp (08C1195h) e->not_virtual_print(); 008C279D mov ecx,dword ptr [e] 008
C27A0 call E::not_virtual_print (08C14A1h)

 二者差了很多行,明顯虛擬函式額外消耗了CPU資源,主要是消耗在了多次開啟指標獲取地址,這也是執行時多型的特點。因為:虛擬函式的呼叫過程是跳到虛擬函式表->開啟虛擬函式表中的虛擬函式指標->依據指標跳到真實函式體所在的位置。而成員函式的執行過程則是直接跳到真實函式體的位置。

捨棄虛擬函式,擁抱成員函式

 然而大多數時候,我們明確知道物件E要呼叫自己重寫的虛擬函式,每次呼叫e->print()都去查詢虛擬函式表是無意義的。要想進一步優化程式的執行時間,只能忍痛捨棄虛擬函式機制。但是與此同時,又希望保留繼承帶來的其他便利性,此時就需要使用Curiously Recurring Template Prattern—奇異遞迴模板模式。

template <typename T>
class D {
public:
    int num;
    void base_print() { reinterpret_cast< T * const>(this)->print(); }
protected:
    D() {}
};
class E :public D<E> {
public:
    E(int i = 0) { num = i; }
    void print() { cout << "I'm a E. my num=" << num << endl; }
    void not_virtual_print() { cout << "not virtual func" << num << endl; }
};

int main()
{
    E* e = new E(1);
    e->print();
    e->not_virtual_print();
    delete e;
    return 0;
}

對應的彙編程式碼變為:

    e->print();
002C28A3  mov         ecx,dword ptr [e]  
002C28A6  call        E::print (02C14ABh)  
    e->not_virtual_print();
002C28AB  mov         ecx,dword ptr [e]  
002C28AE  call        E::not_virtual_print (02C14A1h)  

 這樣呼叫e->print()的時候就不涉及虛擬函式機制了,直接當做型別E的成員函式呼叫。而基類中D的base_print()是用來保持多型特性的,之後會介紹。
 可以看到CPU消耗減小了。遞迴模板的實現原理是這樣的:基類D是模板,E繼承了模板D的一個具體化類D<E>D<E>一開始是不能完成具體化的,因為E還沒有完成繼承。所以順序是E繼承了void base_print()(此時該函式中的T還沒有具體化)->用E具體化D<E>(此時void base_print()中的T已經具體化為了E)->具體化E中的void base_print()reinterpret_cast< E * const>(this)->print();

保持多型特性

 考慮如下的程式碼:

template <typename T>
class D {
public:
    int num;
    void base_print() { reinterpret_cast< T * const>(this)->print(); }
protected:
    D() {}

};
class E :public D<E> {
public:
    E(int i = 0) { num = i; }
    void print() { cout << "I'm a E. my num=" << num << endl; }
    void not_virtual_print() { cout << "not virtual func" << num << endl; }
};
class F :public D<F> {
public:
    F(int i = 0) { num = i; }
    void print() { cout << "I'm a F. my num=" << num << endl; }
    void not_virtual_print() { cout << "not virtual func" << num << endl; }
};
template <typename T>
void print(T* d)
{
    d->base_print();
}

int main()
{
    E* e = new E(1);
    F* f = new F(2);
    e->base_print();
    e->not_virtual_print();
    print(e);
    print(f);
    delete e;
    delete f;
    return 0;
}

 添加了新的模板函式print(),把多型的實現委託給它來實現,這樣就能在編譯期間確定模板函式print(),所以這就叫編譯期多型,或者靜態多型(static polymorphism)。缺點是對於每一個從D派生出來的類,都要具體化一個D<T>和一個模板函式print(),這增加了程式碼的大小。所以到底是使用靜態多型還是動態多型,需要程式設計人員根據實際情況權衡。

總結

 動態多型可以在執行時確定派生類的資訊,缺點是需要多次進行指標的解引用操作,消耗CPU。靜態多型在編譯期間就能確定派生類的資訊,缺點是程式碼大小會變大。
關於動態多型的原理見我的另一篇文章:http://blog.csdn.net/popvip44/article/details/72763004