多态
多态字面理解就是有多种形式和形态,编程中专门指一种可以将不同的行为关联到一个泛型符号的能力。
多态是面向对象编程的基石之一,C++主要通过类的继承和虚函数来实现多态。
多态又可以分为动多态和静多态,主要的区别是多态的表现形式是在运行期处理,还是在编译期处理。
动多态
动多态是在运行期处理多态行为,常常说的“多态”也大多指这种形式。
多态的设计思想在于:识别相关对象类型中的一组公共功能,并将其声明为公共基类中的虚函数接口。
1 | struct Coord { |
使用类的多态,需要将一些公共接口用虚基类抽象出来,比如上述抽象出了一个图形对象的虚基类,有两个方法,draw()
会画出该图形的形状,而centerOfGravity()
会返回图形的重心位置。
我们将继承这个虚基类,让各种图形实现自己对应的公共方法。
1 | class Circle : public GeoObj { |
有三个图形继承了上面的虚基类,分别是圆、直线和矩形,不妨假设它们都已经实现了对应的公共虚函数。然后我们会通过多态的方式使用它们。
1 | void myDraw(const GeoObj& obj) { |
我们又定义了几个函数,分别用来画出形状,计算图形重心间的距离和批量画出数组中的所有图形的形状。
可以看到,画出单个图形形状和计算重心距离的函数使用了虚基类GeoObj的引用作为入参,批量绘制图形形状的接口使用虚基类的指针作为动态数组vector的元素类型。
在main函数中,实例化了一个直线图形和两个圆的对象,它们可以直接作为我们定义的函数的入参,这就是多态。通过虚基类的引用或指针,可以访问派生类中重新实现的方法,而这些方法会根据实现的不同,表现出不同的行为。
多态最优秀的特点就是可以批量地处理异类对象集合的能力。
静多态
模板也可以实现多态,而且模板并不依赖包含公共行为的虚基类,只需要调用的对象有着同名的函数即可。
1 | template<typename GeoObj> |
使用这个模板函数,图形不再需要继承虚基类GeoObj,只需要在各自内部有名为draw
的成员函数即可。
换句话说,模板实际上天然具有了多态的特性,它会在编译期根据指定的模板参数类型去调用恰当的函数,所以也被叫做静多态。
不过,静多态不再能够处理void drawElements(const std::vector<GeoObj*>& elems)
,因为模板参数必须通过某种方式显式地指定,就不再能够处理异类对象集合这种情况。这是静多态静态特性所施加的约束,换取的是性能和类型安全方面的一些优势。
总结
- 通过继承实现的多态是绑定和动态的:
绑定: 意味着参与多态行为的类型的接口是通过继承公共基类来获取的,而这个公共基类是事先设计好的。
动态: 意味着接口的绑定是在运行期(动态地)完成的。
- 通过模板实现的多态是非绑定的和静态的:
非绑定: 意味着参与多态行为的类型的接口并不是预先确定的。
静态:意味着接口的绑定是在编译期(静态地)完成的。
优缺点
动多态具有的优点:
- 优雅地处理异类集合。
- 可执行代码的大小可能更小。
- 代码可以完全编译,不必发布任何实现源码(分发模板库通常需要分发模板实现的源码)。
静多态具有的优点:
- 内置类型的集合容易实现(不必继承公共基类)。
- 生成的代码效率更高(因为不需要通过指针来调用,可以更频繁地内联非虚函数)。
- 如果程序仅需要执行部分接口,仍可以只提供对应的部分接口。
动多态与静多态并非是非此即彼的关系,实际开发中往往是结合着使用的。通过合理地使用两者,可以得到一些更灵活和强大的代码实现。
使用概念
静多态强大之处也往往是让人诟病的一点,更高的灵活性意味着没有公共的接口类设计,这样可能会造成一些难以理解的情况发生,甚至是一些能通过编译但是完全不符合预期的行为。
也就是说,模板太“无法无天”了,我们亟需一些限制来约束模板的行为。为此,C++17标准提出了**概念(concept)**关键字(C++20中已经成为了标准的一部分)。
1 | template<typename T> |
这里使用关键字concept定义了GeoObj的概念,它将模板参数类型约束为具有适当返回类型的成员函数draw
和centerOfGravity
的类型。
于是我们可以使用这个概念来约束模板参数:
1 | template<typename T> |
在实例化模板时,会判断模板参数在概念约束下的执行结果,只有通过了概念的约束,判断为true时才会被实例化。
新形式的设计模式
传统的桥接模式:
传统的桥接模式通过继承来实现:在接口类中定义一个公共基类的指针,通过动多态在多个不同的实现之间进行切换。这就需要提前设计好一个公共虚基类,而采用模板的的方式来实现时,就可以跳过设计公共基类的过程。