泛型编程
Bjarne: 如果你面对的问题既需要某些运行期决议(需要面向对象编程),又具有一些能够从编译期决议中获益的方面(泛型编程的用武之地)的话,那么你就需要将面向对象编程和泛型编程结合起来。例如,面向对象编程的经典例子 — 将一个保存了shape的容器中的所有元素都显示出来就属于这类问题。几十年前我第一次在Simula中看到过这个例子,后来直到遇到了泛型编程,我才看到它的改进实现。考虑以下代码:
void draw_all(vector<Shape*>& vs)
{
for (int i=0; i<vs.size(); ++i) vs[i]->draw();
}
我猜想这并不能被看作纯粹的面向对象编程,因为我直接利用了“vs是一个装有Shape*元素的vector”这个事实。毕竟,类型的参数化通常是被认为属于泛型编程的范畴。我们也可以消除这种对静态类型信息的使用(所谓“不纯粹的面向对象编程”):
void draw_all(Object* container)
{
Vector* v = dynamic_cast<Vector*>(container);
for (int i=0; i<v.size(); ++i)
{
Shape* ps = dynamic_cast<Shape*>(v[i]);
ps->draw();
}
}
但凡鼓励以上这种风格的语言,其语法通常都比较漂亮,然而这个例子却说明了当你把静态类型信息的使用减至最小的时候发生了什么。如今,在C++或其它语言中,仍然有人在使用这种风格。我只是希望他们在错误处理方面有系统化的准备。
在前一个例子中,vector<Shap*>依赖于对泛型编程的一个最简单的运用:vector的元素类型被参数化了,而且我们的示例代码正获益于此。在这个方向上我们还可以走得更远,即推而广之到所有标准库容器身上:
template<class Container> void draw_all(Container& cs)
{
for (typename C::iterator p=cs.begin(); p!=cs.end(); ++p)
(*p)->draw();
}
例如,这段代码就既可以作用于vector上,又可以作用于list上。编译期决议确保我们不用为这种泛化处理付出任何运行期额外代价。我们还可以通过在draw_all()的使用接口中运用迭代器,从而进行进一步的泛化处理:
template<class Iter> void draw_all(Iter fist, Iter last)
{
for (; first!=last; ++first)
(*first)->draw();
}
这就使内建数组类型都得到了支持:
Shape* a[max];
// 向a中填充Shape*类型的元素
draw_all(a,a+max);
我们还可以结合运用标准库算法for_each()和函数适配器mem_fun()来消除显式的循环:
template<class Iter> void draw_all(Iter fist, Iter last)
{
for_each(first, last, mem_fun(&Shape::draw);
}
在这些例子中,我们结合了面向对象(对虚函数draw()的调用以及对类继承体系的假设)和泛型编程(参数化的容器和算法)技术。我看不出如果这两种编程风格(即所谓的“范型”)各自独立运用如何达到同样好的效果。这也是一个简单的“多范型编程”的例子。
我认为在设计和编程技术方面,我们还需要做更多的工作,以便确定出“关于何时采用哪种范型以及如何结合运用它们”的更为具体的规则。为此,我们还需要一个比“多范型编程”更好的名字。
注意,这也是一个关于编译错误信息变得可怕的例子,因为我们并没有显式地表达出我们的假设。例如,我们假设容器里的元素类型为Shape*,然而在代码中,这个假设却相当隐晦。这种情况下我们可以使用约束类(此处为Point_to):
template<class Iter> void draw_all(Iter fist, Iter last)
{
Points_to<Iter,Shape*>();
for_each(first, last, mem_fun(&Shape::draw);
}
然而我们又确实很想说明“first和last必须为前向迭代器”:
template<Forward_iterator<Shape*> Iter>
void draw_all(Iter fist, Iter last)
{
for_each(first, last, mem_fun(&Shape::draw);
}
这是“concepts”可以大展拳脚的地方之一。
;-)