运行时的多态,比如C++用虚函数,或者比如有些语言支持委托。这样实际运行的代码是运行时决定的,这样必须要承担一定的开销:
1、间接调用。比如说虚表。2、调用的函数编译期不能确定,所以不能展开。
这样性能上是有很大的损失的。如果这个调用出现在一个循环里,或者被其他地方频繁的调用,效率会有很大的差距。
其实有些时候,我们并不是真的需要真正的运行时多态,有时可能就是编译期的多态。
举例:
class ServerBase { public : virtual void Open() = 0 ; virtual ~ServerBase() {} ; } ; class FtpServer { public : virtual void Open() { // } } ; class HttpServer { public : virtual void Open() { // } } ; 然后你的程序里使用起来是这样的:
ServerBase * pServer = new HttpServer(); pServer->Open() ; 这样你做了个HttpServer。如果这里就是个具体应用,没有要求你运行期间能替换协议,那么这个设计明显有额外的开销。让我们先做几个例子来看看调用一个函数的开销。
例子:
class CPolymorphismBase { public : void MemberFunction() { std::cout << " MemberFunction Base " ; } virtual void VirtualFunction() { std::cout << " VirtualFunction Base " ; } virtual ~ CPolymorphismBase() {} } ; class CPolymorphismDerived : public CPolymorphismBase { public : void MemberFunction() { std::cout << " MemberFunction Derived " ; } virtual void VirtualFunction() { std::cout << " VirtualFunction Derived " ; } } ;
首先我们来看看普通的成员函数的开销是怎么样的。
CPolymorphismDerived derivedObject; CPolymorphismDerived * pDerivedObject = new CPolymorphismDerived(); derivedObject.MemberFunction(); pDerivedObject -> MemberFunction();
运行一下,看到汇编内容如下:
derivedObject.MemberFunction(); 00401034 mov eax,dword ptr [__imp_std::cout (40204Ch)] 00401039 push offset string " MemberFunction Derived " (40213Ch) 0040103E push eax 0040103F call std:: operator <<< std::char_traits < char > > (401190h) pDerivedObject -> MemberFunction(); 00401044 mov ecx,dword ptr [__imp_std::cout (40204Ch)] 0040104A push offset string " MemberFunction Derived " (40213Ch) 0040104F push ecx 00401050 call std:: operator <<< std::char_traits < char > > (401190h)
可以看到,函数不但没有虚表的开销,而且已经在这里展开了,编译器替我们完成了inline优化,这样的效率当然是最高的。
然后我们来看虚函数。
CPolymorphismDerived derivedObject; CPolymorphismDerived * pDerivedObject = new CPolymorphismDerived(); CPolymorphismBase * pDerivedObjectBasePoint = pDerivedObject; derivedObject.VirtualFunction(); pDerivedObject -> VirtualFunction(); pDerivedObjectBasePoint -> VirtualFunction();
运行一下,看到汇编内容如下:
derivedObject.VirtualFunction(); 0040103B mov eax,dword ptr [__imp_std::cout (40204Ch)] 00401040 push offset string " VirtualFunction Derived " (40213Ch) 00401045 push eax 00401046 call std:: operator <<< std::char_traits < char > > (4011A0h) pDerivedObject -> VirtualFunction(); 0040104B mov edx,dword ptr [esi] 0040104D mov eax,dword ptr [edx] 0040104F add esp, 8 00401052 mov ecx,esi 00401054 call eax pDerivedObjectBasePoint -> VirtualFunction(); 00401056 mov edx,dword ptr [esi] 00401058 mov eax,dword ptr [edx] 0040105A mov ecx,esi 0040105C call eax
栈上分配出来的那个对象,虽然是虚函数,但是编译期是确定的,所以没有虚表的开销,函数也成功展开了。堆上new出来的那个对象,就不走运了。不管是基类指针还是派生类指针去操作,都有虚表的开销,同时函数也无法展开。
这是什么意思呢?就是说即时一开始的例子改成这样也一样性能很低下
// ServerBase* pServer = new HttpServer(); // 既然这样写性能低。。。 HttpServer * pServer = new HttpServer(); // 那我换成这样写吧!!! pServer->Open() ; // but,即使不用基类指针操作,效率也是一样低。
我们来总结一下吧
性能 栈上分配 堆分配 + 父类指针 堆分配 + 子类指针 普通成员函数 高 高 高 虚函数 高 低 低
下面切入正题。要让用户使用起来性能得到保障,就只有用普通成员函数了。(你总不能强制用户必须使用栈对象吧?)
既然是普通成员函数,那也就没ServerBase什么事了。(既然不使用虚函数,那么一个基类的指针也就调用不到子类了,所以这里弄个基类没什么用了。)所以,代码修改如下了:
class FtpServer { public : void Open() { // } } ; class HttpServer { public : void Open() { // } } ;
所以使用的时候就可以这么写了:
typedef HttpServer ServerBase; typedef HttpServer Server; ServerBase * pServer = new Server(); pServer->Open(); //
这样,就可以了,什么时候需要一个Ftp的版本了,就可以简单修改头两行代码,别的都不用改,这样就是一个FtpServer了:
typedef FtpServer ServerBase; typedef FtpServer Server; ServerBase * pServer = new Server(); pServer -> Open(); //
这里也可以用模板:
template < class T > class App { public : typedef T ServerBase; typedef T Server; static void main() { ServerBase * pServer = new Server(); pServer->Open() ; // } } ;
App<FtpServer>::main(); // 这就是Ftp的 //App<HttpServer>::main(); // 这就是Http的
这就是编译期的多态,都用普通成员函数,使用到多态对象的类可以使用模板来简化多态时的修改(都用模板,然后在外部整体一个typedef就可以把所有需要多态的类都提纯出来,一个修改就可以让全程序得到效果)。
当然,并不是到这就结束了。。。你的HttpServer和FtpServer都改好了。。。但是你的BOSS过来和你说:兄弟,新需求,需要允许运行时修改协议,允许热配置。然后你就发现:坏了,二了,咱已经改成编译期多态的了。。。
这该怎么办呢?怎么让你设计的类既能配置成编译期多态而且不损效率,又能配置成运行时多态呢?
这里可以用个Adapter来做。
template < class T > class Server_Adapter : public ServerBase { public : Server_Adapter() { } virtual ~ Server_Adapter() { } virtual void Open() { m_server.Open(); } private : T m_server; } ;
这样就OK了。使用者如果需要编译期多态,就像上一个例子那么用就可以了,性能没问题;如果需要运行期多态,那就这么写:
typedef ServerBase ServerBaseObject; typedef Server_Adapter < HttpServer > HttpServerObject; typedef Server_Adapter < FtpServer > FtpServerObject; ServerBaseObject * pServer = NULL; int nServerType = ; // 这里可以动态得到,从配置文件里头读啊什么的,反正都行,运行时随便改。 switch (nServerType) { case 0 : { pServer = new HttpServerObject(); } break ; case 1 : { pServer = new FtpServerObject(); } break ; default : // break ; } pServer -> Open(); //
这样就运行时多态了,一点毛病没有。和一开始的方案比,没有一丝性能损失。也许你要问了:这不是多包了一层么?调用Open的时候,要多一次函数调用呀?当然不会了。。。作为Adapter的T来说,编译期是确定的,所以内部的对象m_server作为一个栈对象,调用的函数是可以展开的。我们还是看一下具体的汇编代码:
pServer-> Open(); 00401030 mov eax,dword ptr [__imp_std::cout (402050h)] 00401035 push eax 00401036 call std:: operator <<< std::char_traits < char > > (401190h) 0040103B pop ecx
可见,这里的代码展开了,您可以试验一下在HttpServer::Open和FtpServer::Open那里下个断点,就会发现根本断不下来,因为那里的代码已经展开在Server_Adapter<HttpServer>和 Server_Adapter<FtpServer>里头了,根本没有间接调用的问题。
这样,一个兼顾效率和灵活性的完美类库产生了。