目录
第14章 模板的多态威力
多态是一种能够令单一的泛型标记关联不同特定行为的能力。 对面向对象的程序设计范例而言, 多态可以说是一块基石。 在C++中, 这块基石主要是通过继承和虚函数来实现的。 由于这两个机制(继承和虚函数) 都是(至少一部分) 在运行期进行处理的, 因此我们把这种多态称为动多态(dynamic polymorphism) ; 我们平常所谈论的C++多态指的就是这种动多态。 然而, 模板也允许我们使用单一的泛型标记, 来关联不同的特定行为; 但这种(借助于模板的) 关联是在编译期进行处理的, 因此我们把这种(借助于模板的) 多态称为静多态(static polymorphism) , 从而和上面的动多态区分开来。 在这一章里, 我们将重温这两种形式的多态, 然后讨论: 在何种情况下, 应该使用哪一种多态。
14.1 动多态
、
可以如下组织具体的代码:
/ 画任意一个GeoObj
void myDraw(GeoObj const& obj)
{
obj.draw(); // 根据对象的类型来调用对应的draw()
}
//计算两个 GeoObj对象重心之间的距离
Coord distance(GeoObj const& x1, GeoObj const& x2)
{
Coord c = x1.center_of_gravity() - x2.center_of_gravity();
return c.abs(); // 返回坐标的绝对值
}
// 画出属于异类集合的GeoObj对象
void drawElems(std::vector<GeoObj*> const& elems)
{
for (unsigned i = 0; i < elems.size(); ++i) {
elems[i]->draw(); // 根据元素的类型来调用相应的 draw()
}
}
int main()
{
Line l;
Circle c, c1, c2;
myDraw(l); // myDraw(GeoObj&) => Line::draw()
myDraw(c); // myDraw(GeoObj&) => Circle::draw()
distance(c1, c2); // distance(GeoObj&,GeoObj&)
distance(l, c); // distance(GeoObj&,GeoObj&)
std::vector<GeoObj*> coll; // 元素类型互异的集合
coll.push_back(&l); // 插入一条直线
coll.push_back(&c); // 插入一个圆
drawElems(coll); // 画不同种类的 GeoObj对象
}
对于动多态而言, 最引入注目的特性或许是处理异类容器的能力。上面例子中的drawElems就阐述了这个概念, 诸如下面的简单表达式:
elems[i]->draw();
将会根据被迭代元素的类型, 而调用不同的成员函数。
14.2 静多态
模板也能够被用于实现多态。 然而, 这种多态并不依赖于在基类中包含公共行为的因素; 但仍然存在一种隐式的公共性, 即应用程序的不同“形状(即类型) ”都必须支持某些使用公共语法的操作(也就是说,相关的函数必须具有相同的名称) 。 另外, 具体类之间的定义是互相独立的(见图14.2) 。 于是, 当用具体类对模板进行实例化的时候, 这种多态的威力就显示出来了。
例如, 前面小节中的myDraw()函数:
void myDraw (GeoObj const& obj) // GeoObj 是一个抽象基类
{
obj.draw();
}
大概可以被改写如下:
template <typename GeoObj>
void myDraw (GeoObj const& obj) // GeoObj 是模板参数
{
obj.draw();
}
通过比较myDraw()的这两个实现, 我们可以看出: 主要的区别在于后一个GeoObj的规范是模板参数, 而不是一个公共基类。 然而, 在这个现象的背后, 还存在更多本质的差别。 例如, 使用动多态, 我们在运行期只具有一个myDraw()函数, 而如果使用模板, 我们则可能具有多个不同的函数, 诸如myDraw<Line>()和myDraw<Circle>()。
/// 具体的几何对象类 Circle
// - 并没有派生自任何其他的类
class Circle {
public:
void draw() const;
Coord center_of_gravity() const;
…
};
// 具体的几何对象类Line
// - 并没有派生自任何其他的类
class Line {
public:
void draw() const;
Coord center_of_gravity() const;
…
};
…
现在, 使用这些类的应用程序看起来如下所示:
// 画出任意 GeoObj
template <typename GeoObj>
void myDraw(GeoObj const& obj)
{
obj.draw(); // 根据对象的类型调用相应的draw()
}
// 计算两个GeoObj对象之间重心的距离
template <typename GeoObj1, typename GeoObj2>
Coord distance(GeoObj1 const& x1, GeoObj2 const& x2)
{
Coord c = x1.center_of_gravity() - x2.center_of_gravity();
return c.abs(); // 返回坐标的绝对值
}
//画出属于异类集合的GeoObj对象
template <typename GeoObj>
void drawElems(std::vector<GeoObj> const& elems)
{
for (unsigned i = 0; i < elems.size(); ++i) {
elems[i].draw(); // 根据元素的类型调用相应的draw()
}
}
int main()
{
Line l;
Circle c, c1, c2;
myDraw(l); // myDraw<Line>(GeoObj&) => Line::draw()
myDraw(c); // myDraw<Circle>(GeoObj&) => Circle::draw()
distance(c1, c2);
//distance<Circle,Circle>(GeoObj1&,GeoObj2&)
distance(l, c); // distance<Line,Circle>(GeoObj1&,GeoObj2&)
// std::vector<GeoObj*> coll; // 错误: 异类集合在这里是不允许的
std::vector<Line> coll; // 正确: 同类集合在这里是允许的coll.push_back(l);
// 插入一条直线
drawElems(coll); // 画出所有的直线
}
在上面的distance()函数中, 有一点和myDraw()函数是不同的: 我们已经不再使用GeoObj作为一个具体的参数类型, 而是提供了两个模板参数GeoObj1和GeoObj2。 通过使用这两个不同的模板参数, 距离计算函数就可以接受由两个不同的几何对象类型所组成的各种组合:
distance(l,c); // distance<Line,Circle>(GeoObj1&,GeoObj2&)
然而, 我们在此再也不能透明地处理异类的集合; 这也是静多态的静态特性所强加的约束: 所有的类型都必须能够在编译期确定。 但我们可以为不同的几何对象类型引入不同的集合; 而且, 集合的元素类型也不再局限于指针, 从而能够在性能和类型安全方面给我们带来一些显著的好处。
14.3 动多态和静多态
我们来对多态进行分类, 并对这两种多态进行比较。
14.3.1 术语
动多态和静多态为不同的C++编程idioms提供了支持:
•通过继承实现的多态是绑定的和动态的:
绑定的含义是: 对于参与多态行为的类型, 它们(具有多态行为)的接口是在公共基类的设计中就预先确定的(有时候也把绑定这个概念称为入侵的或者插入的) 。
动态的含义是: 接口的绑定是在运行期(动态) 完成的。
•通过模板实现的多态是非绑定的和静态的:
非绑定的含义是: 对于参与多态行为的类型, 它们的接口是没有预先确定的(有时也称这个概念为非入侵的或者非插入的) 。
静态的含义是: 接口的绑定是在编译期(静态) 完成的。
因此, 严格地讲, 在针对C++的说法中, 动多态是绑定并且动态的多态的简称, 而静多态则是非绑定并且静态的多态的简称。
14.3.2 优点和缺点
C++的动多态具有下列优点:
•能够优雅地处理异类集合。
•可执行代码的大小通常比较小(因为只需要一个多态函数, 但对于静多态而言, 为了处理不同的类型, 必须生成多个不同的模板实例) 。
•可以对代码进行完全编译; 因此并不需要发布实现源码(但是,分发模板库通常都需要同时分发模板实现的源代码) 。
C++的静多态则具有下列优点:
•可以很容易地实现内建类型的集合。 更广义地说, 并不需要通过公共基类来表达接口的共同性。
•所生成的代码效率通常都比较高(因为并不存在通过指针的间接调用, 而且, 可以进行演绎的非虚拟函数具有更多的内联机会) 。
•对于只提供部分接口的具体类型, 如果在应用程序中只是使用到这一部分接口, 那么也可以使用该具体类型; 而不必在乎该类型是否提供其他部分的接口。
通常而言, 与动多态相比, 静多态被认为具有更好的类型安全性;因为静多态在编译期会对所有的绑定操作进行检查。例如, 假设我们尝试把一个错误类型的对象插入到一个容器中, 如果这个容器是根据模板实例化而生成的话, 那么几乎不会有危险, 因为在编译期就可以检查出这个错误; 但如果该容器所期望的元素是指向公共基类的指针, 那么这些指针最后很有可能会指向不同类型的完整对象, 而这就有可能会插入错误类型的对象。