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