今天的C++已经是个多重泛型编程语言(multiparadigm programming lauguage),一个同时支持过程形式(procedural)、面向对象形式(object-oriented)、函数形式(functional)、泛型形式(generic)、元编程形式(metaprogramming)的语言。 这些能力和弹性使C++成为一个无可匹敌的工具,但也可能引发使用者的某些迷惑,比如多态。在这几种编程泛型中,面向对象编程、泛型编程以及很新的元编程形式都支持多态的概念,但又有所不同。 C++支持多种形式的多态,从表现的形式来看,有虚函数、模板、重载等,从绑定时间来看,可以分成静态多态和动态多态,也称为编译期多态和运行期多态。
本文即讲述这其中的异同。注意泛型编程和元编程通常都是以模板形式实现的,因此在本文中主要介绍基于面向对象的动态多态和基于模板编程的静态多态两种形式。另外其实宏也可以认为是实现静态多态的一种方式,实现原理就是全文替换,但C++语言本身就不喜欢宏,这里也忽略了“宏多态”。
什么是动态多态?
动态多态的设计思想:对于相关的对象类型,确定它们之间的一个共同功能集,然后在基类中,把这些共同的功能声明为多个公共的虚函数接口。各个子类重写这些虚函数,以完成具体的功能。客户端的代码(操作函数)通过指向基类的引用或指针来操作这些对象,对虚函数的调用会自动绑定到实际提供的子类对象上去。
从上面的定义也可以看出,由于有了虚函数,因此动态多态是在运行时完成的,也可以叫做运行期多态,这造就了动态多态机制在处理异质对象集合时的强大威力(当然,也有了一点点性能损失)。
看代码:
namespace DynamicPoly { class Geometry { public: virtual void Draw()const = 0; }; class Line : public Geometry { public: virtual void Draw()const{ std::cout << "Line Draw() "; } }; class Circle : public Geometry { public: virtual void Draw()const{ std::cout << "Circle Draw() "; } }; class Rectangle : public Geometry { public: virtual void Draw()const{ std::cout << "Rectangle Draw() "; } }; void DrawGeometry(const Geometry *geo) { geo->Draw(); } //动态多态最吸引人之处在于处理异质对象集合的能力 void DrawGeometry(std::vector<DynamicPoly::Geometry*> vecGeo) { const size_t size = vecGeo.size(); for(size_t i = 0; i < size; ++i) vecGeo[i]->Draw(); } } void test_dynamic_polymorphism() { DynamicPoly::Line line; DynamicPoly::Circle circle; DynamicPoly::Rectangle rect; DynamicPoly::DrawGeometry(&circle); std::vector<DynamicPoly::Geometry*> vec; vec.push_back(&line); vec.push_back(&circle); vec.push_back(&rect); DynamicPoly::DrawGeometry(vec); }
动态多态本质上就是面向对象设计中的继承、多态的概念。动态多态中的接口是显式接口(虚函数),比如,
void DoSomething(Widget& w) { if( w.size() > 0 && w != someNastyWidget) { Widget temp(w); temp.normalize(); temp.swap(w); } }
对于上面的代码,这要求:
- 由于w的类型被声明为Widget,所以w必须支持Widget接口,且通常可以在源码中找出这些接口(比如Widget.h),因此这些接口也就是显示接口;
- Widget可能只是一个基类,他有子类,也就是说Widget的接口有可能是虚函数(比如上面的normalize),此时对接口的调用就表现出了运行时多态;
什么是静态多态?
静态多态的设计思想:对于相关的对象类型,直接实现它们各自的定义,不需要共有基类,甚至可以没有任何关系。只需要各个具体类的实现中要求相同的接口声明,这里的接口称之为隐式接口。客户端把操作这些对象的函数定义为模板,当需要操作什么类型的对象时,直接对模板指定该类型实参即可(或通过实参演绎获得)。
相对于面向对象编程中,以显式接口和运行期多态(虚函数)实现动态多态,在模板编程及泛型编程中,是以隐式接口和编译器多态来实现静态多态。
看代码:
namespace StaticPoly { class Line { public: void Draw()const{ std::cout << "Line Draw() "; } }; class Circle { public: void Draw(const char* name=NULL)const{ std::cout << "Circle Draw() "; } }; class Rectangle { public: void Draw(int i = 0)const{ std::cout << "Rectangle Draw() "; } }; template<typename Geometry> void DrawGeometry(const Geometry& geo) { geo.Draw(); } template<typename Geometry> void DrawGeometry(std::vector<Geometry> vecGeo) { const size_t size = vecGeo.size(); for(size_t i = 0; i < size; ++i) vecGeo[i].Draw(); } } void test_static_polymorphism() { StaticPoly::Line line; StaticPoly::Circle circle; StaticPoly::Rectangle rect; StaticPoly::DrawGeometry(circle); std::vector<StaticPoly::Line> vecLines; StaticPoly::Line line2; StaticPoly::Line line3; vecLines.push_back(line); vecLines.push_back(line2); vecLines.push_back(line3); //vecLines.push_back(&circle); //编译错误,已不再能够处理异质对象 //vecLines.push_back(&rect); //编译错误,已不再能够处理异质对象 StaticPoly::DrawGeometry(vecLines); std::vector<StaticPoly::Circle> vecCircles; vecCircles.push_back(circle); StaticPoly::DrawGeometry(circle); }
静态多态本质上就是模板的具现化。静态多态中的接口调用也叫做隐式接口,相对于显示接口由函数的签名式(也就是函数名称、参数类型、返回类型)构成,隐式接口通常由有效表达式组成, 比如,
template<typename Widget,typename Other> void DoSomething(Widget& w, const Other& someNasty) { if( w.size() > 0 && w != someNasty) //someNastyT可能是是T类型的某一实例,也可能不是 { Widget temp(w); temp.normalize(); temp.swap(w); } }
这看似要求:
- 类型T需要支持size、normalize、swap函数,copy构造函数,可以进行不等比较
- 类型T是在编译期模板进行具现化时才表现出调用不同的函数,此时对接口的调用就表现出了编译期时多态。
但是,
- size函数并不需要返回一个整型值以和10比较,甚至都不需要返回一个数值类型,唯一的约束是它返回一个类型为X的对象,且X对象和int类型(数值10的类型)可以调用一个operator >,这个operator>也不一定非要一个X类型的参数不可,它可以通过隐式转换能将X类型转为Y类型对象,而只需要Y类型可以和int类型比较即可(好绕口,请看,这也侧面印证了模板编程编译错误很难解决)。
- 同样类型T并不需要支持operator!=,而只需要T可以转为X类型对象,someNastyT可以转为Y类型对象,而X和Y可以进行不等比较即可。
动态多态和静态多态的比较
静态多态
优点:
- 由于静多态是在编译期完成的,因此效率较高,编译器也可以进行优化;
- 有很强的适配性和松耦合性,比如可以通过偏特化、全特化来处理特殊类型;
- 最重要一点是静态多态通过模板编程为C++带来了泛型设计的概念,比如强大的STL库。
缺点:
- 由于是模板来实现静态多态,因此模板的不足也就是静多态的劣势,比如调试困难、编译耗时、代码膨胀、编译器支持的兼容性
- 不能够处理异质对象集合
动态多态
优点:
- OO设计,对是客观世界的直觉认识;
- 实现与接口分离,可复用
- 处理同一继承体系下异质对象集合的强大威力
缺点:
- 运行期绑定,导致一定程度的运行时开销;
- 编译器无法对虚函数进行优化
- 笨重的类继承体系,对接口的修改影响整个类层次;
不同点:
- 本质不同,静态多态在编译期决定,由模板具现完成,而动态多态在运行期决定,由继承、虚函数实现;
- 动态多态中接口是显式的,以函数签名为中心,多态通过虚函数在运行期实现,静态多台中接口是隐式的,以有效表达式为中心,多态通过模板具现在编译期完成
相同点:
- 都能够实现多态性,静态多态/编译期多态、动态多态/运行期多态;
- 都能够使接口和实现相分离,一个是模板定义接口,类型参数定义实现,一个是基类虚函数定义接口,继承类负责实现;