多态是 C++中面向对象技术的核心机制之一 包含静态多态和动态多态 它们之间有一定的相似性 述了这种相似性 并重点论述了以模板实现的静态多态的应用范围
关键词 动态多态 静态多态 模板多态 概念 模型 标准模板库
但是应用范围不同
C++支持多种风格的编程模式 称之为编程范型 C++
支持的编程范型包括面向过程的 基于对象的 面向对象的和泛型编程
通过指针和引用来支持多态 是面向对象的编程范型区别于基于对象的编程范型的本质所在 所谓多态 是指通过单一的标识支持不同的特定行为的能力 C++支持多种形式的多态 从绑定时间来看 可以分成静态多态和动态多态也称为编译期多态和运行期多态 从表现的形式来看 有虚函数 模板 重载和转换[1]
由于静态多态在时间和空间上都比动态多态表现得好因此在其他的条件相同的情况下 应该更多地使用静态多态
2 模板多态的应用范围2.1 代码实例
在类型需要参数化的时候使用模板多态 为了说得更明白 使用一个具体的代码例子 具体要求是 提供一个空间点的结构 可能是二维或者三维的 同时提供一些算法 例如两点之间距离 点序列总长度
(1)使用虚函数支持的动态多态class Point//虚基类
{//提供访问 x y z 的纯虚函数};
class Point2D : public Point //二维子类{//实现访问 x y z 的纯虚函数};
class Point3D : public Point//三维子类{//实现访问 x y z 的纯虚函数};
提供一个求两个点距离的函数
double Distance(const Point& pt1, const Point& pt2){ double dx = pt1.x() - pt2.x();
double dy = pt1.y() - pt2.y();
double dz = pt1.z() - pt2.z();
return sqrt(dx * dx + dy * dy + dz * dz); }
(2)使用模板多态 提供两个 POD 类72
class TPoint2D
{//实现访问 x y z 的函数};class TPoint3D
{/实现访问 x y z 的函数};
也提供一个求两个点距离的函数对象
template<typename point_t>
class Distance_op:: public binary_function<const point_t&, const
point_t&, double>{public:
double operator() (const point_t& pt1, const point_t& pt2){ double dx = pt1.x() - pt2.x();
double dy = pt1.y() - pt2.y();
double dz = pt1.z() - pt2.z();
return sqrt(dx * dx + dy * dy + dz * dz); }
};
在上面的两种结构当中 二维结构都是三维结构的一种退化情况(二维结构在 z 坐标的取得值返回 0 设置值是空函数)
一些 但是不足以让我们舍弃虚函数结构而选择模板结构2.2 对象本身的集合
目前看来这两个的结构都很优雅 虽然模板的效率会高
求一个点系列的总长度的算法 使用模板的结构如下
template<typename FrowardIterator, typename BinaryOperation >
double adjacent_op(FrowardIterator first, FrowardIterator last,BinaryOperation op)
{ double result(0.);FrowardIterator next = first;while(++next != last){
result += op(*first,*next);
first = next;}return result; }
void test()//测试
{ vector<TPoint2D> tpts(20);
double tlen = adjacent_op(tpts.begin(),tpts.end(),Distance_op<
TPoint2D>()); }
如果使用虚函数结构 也可以这样
double Length(const Point* first, const Point* last)
{
double result(0.);
const Point* next = first;
while(++next != last)
{
result += Distance(*first,*next);
first = next;
}
return result;
}
但这是错误的 原因是 sizeof(Point) 不等于 sizeof(Point2D) ++next指针的位置不是指向下一个位置的Point2D对象 程序也许会崩溃 更深的原因是 向量 vector 里面放的是 Point2D 对象本身 而不是对象的指针 所以我们期待的对象的多态行为不会发生
纠正这个错误可以有两个方法 (1)改变 Length 函数的参数类型 改成 Length(const Point2D* first, const Point2D*last) 这个方法显然不能令人满意 因为求得三维点序列的总长度 要写一个几乎完全一样的函数 (2)我们的模板辅助函数 adjacent_op 使用如下
void test()
{ vector<Point2D> pts(20);
double tlen = adjacent_op(pts.begin(), pts.end(), Distance); }
在这里需要使用模板来达到目的 说明使用虚函数来解决这个问题是有缺陷的
正如前面所说的 向量 vector 里面放的是 Point2D 对象本身 而不是对象指针 虚函数多态行为不能发生 反过来也给出了模板的一个适用范围 即 当我们的程序使用了对象本身的集合 而不是对象指针的集合 而这些对象的行为表现出共同性的时候 可以考虑使用模板多态
当然 把多态和数组混合使用本身就有问题 这一点在
Scott Meyers 的著作的条款 3 中有过详细的论述[5]
2.3 编译期多态
从另外一个角度看问题 即为什么要把二维和三维的点
结构放在一起编码 从实际的应用来看 很少把二维和三维的点混合在一起使用 即要么我们的程序使用的是二维的点结构 要么使用三维的点结构 而这一点是在编译以前就可以确定 之所以把这两种结构放在一起考虑 主要是因为我们的算法对这两种结构表现出共同性 我们的求距离的Distance 算法 求总长度的 Length 算法 并不依赖于点结构的维数 所以目的是要复用这些算法 而不在每种点结构下重写这些算法
考虑一个使用面向对象技术的典型应用 即绘制点 线和面等几何体的跟客户交互的应用程序 虽然点线面几何体的存储结构差别很大 但是它们在某些行为上表现出共同性例如这些几何体的绘制 偏移等 可以把这些共同的行为抽象出来 以公有函数接口的形式放在一个虚基类里面 让子类继承并实现这些接口 在这种情况下 相对于模板技术虚函数是合适的 原因是 我们的程序响应用户的输入 操纵几何体的时候 这些几何体的具体类型完全是在程序运行期决定的 而不是在编译期决定的
所以 如果我们的算法或者构件表现出共同的行为 而
这些行为是可以在编译期决定的 可以考虑使用模板多态用这一点来考察标准模板库 STL STL 提供的容器类和针对容器类的算法都属于这个范畴 虽然这些容器可以存放不同类型的对象 但是这些对象的类型和大小是编译前决定的不会在运行期改变
2.4 policy-based 的程序设计
采用模板的结构实现点的结构 可以应用于二维和三维
的情况 我们的算法只要在使用的时候配置不同的参数就可以了 为了具有更大的通用性 可以把维数推广到 N 维
template<int N> class Coord{ double coords_[N];
public:
double get(int index) const{//返回在 index 纬度上的值}void set(int index, double value){//设置在 index 纬度上的值} };
template<int N> double DistanceN(const Coord<N> & pt1,constCoord<N> & pt2)
{//求得距离}
N 的大小由使用这个泛型算法或构件的用户在编译期选
定 用户可能还有其他的选择 例如在不同的线程的环境下面 数据是否加锁 但是除了这种加锁的操作 其他的代码没有改变 应该复用这些代码 并且把加锁的选择交到用户的手中 用户的不同选择体现为用户的一种策略 编程者应该充分的考虑这些策略 体现在代码当中
这种基于策略考虑的程序设计称为 policy-based 的程序设计 这样的设计方法在 Andri Alexandrescu 的著作中有过充分的论述[4] 并且展示了这种方法的巨大威力 policy-based程序设计利用多重继承和模板机制 把各种要考虑的策略设计成为高度可组装化的基本单位 用户可以灵活地选择 配置自己的应用程序 一般来说 这样的策略应该是正交的[4]
policy-based 的设计方法在很多的情况下首先考虑复杂的情况 然后把简单的情况看成是复杂情况的一种退化 考虑不同的线程环境 多线程环境下的加锁是确实的行为 而单线程则是空函数 这种技术在微软的 ATL 中有大量的应用
如果我们的代码需要表现出某种策略的选择 这种策略依赖于客户端代码的编译前配置 而如果不考虑这些策略代码表现出很大的共同性 在这种情况下可以考虑使用模板多态
2.5 效率
相对于其他的语言 例如 Java 效率是 C++的一个重要
的优势 模板多态是静态的多态 相对于虚函数的多态 当然更有效率 这一点在某些对效率要求很高的程序 例如数学库 中 显得尤为重要 有时候用模板来实现一些数学运算 也是为了减少读写次数 避免临时变量的产生 因为有的时候临时变量的产生是一个严重的性能障碍 尤其是对于高维的向量和矩阵 在文献[3]中 针对这一专题作了专门的论述 并且提出了表达式模板(Expression Template)的概念 使得浮点数组向量的运算既能保持优雅的结构 又能够获得高效率[3]
总而言之 当我们的程序对效率要求很高的情况下 可以考虑使用模板多态
3 结论
在 C++中 C++同时支持这两种编程范型的代码程序 它们的本质不同点在于 模板是静态的技术 在编译期决定 虚函数更侧重于运行期 适合描述动态的行为 虚函数通过间接层增加编译器防火墙 模板取消间接层增加效率 模板有自己的使用范围 一般来说 在下面的情况下 更适合于使用模板 而不是虚函数 (1)当该文的程序使用了对象本身的集合 而不是对象指针的集合 而这些对象的行为表现出共同性的时候可考虑使用模板多态 (2)如果该文的算法或者构件表现出共同的行为 而这些行为是可以在编译期决定 可以考虑使用模板多态 (3)如果该文的代码需要表现出某种策略的选择这种策略依赖于客户端代码的编译前配置 而如果不考虑这些策略 代码表现出很大的共同性 在这种情况下可以考虑模板多态和虚函数多态是可以共存的 因为
使用模板多态 (4)该文的程序对效率要求很高的情况下 可考虑使用模板多态