C++多态:深入CRTP,理解编译期的多态

本文对比了虚函数与非虚函数在CPU资源消耗上的差异,介绍了如何通过奇异递归模板模式减少虚函数带来的额外开销,并探讨了动态多态与静态多态之间的权衡。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

虚函数带来的额外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]  
008C27A0  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.youkuaiyun.com/popvip44/article/details/72763004

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值