20180326 C++ 绝不重新定义继承而来的缺省参数值
诚然在C++中,只能继承两种函数:虚(virtual)函数和非虚(non-virtual)函数。而重新定义一个继承而来的非虚(non-virtual)函数永远是错误的。所以本节主要讨论的就是“继承一个带有缺省参数值的虚(virtual)函数”。
我们知道:
/***************************************************/
/**** 虚(virtual)函数是动态绑定 ****/
/**** 而缺省参数值确实动态绑定 ****/
/***************************************************/
静态绑定又叫前期绑定(early binding);动态绑定又叫后期绑定(late binding)。
对象的所谓静态类型(static type),是指它在程序中被声明时所采用的类型,eg:
//一个用以描述几何形状的class
class Shape{
public:
enum ShapeColor{Red,green,Blue};
//所有形状都必须提供一个函数,用来描绘出自己
virtual void draw (ShapeColor color = Red) const;
...
};
class Rectangle:public Shape{
public:
virtual void draw(ShapeColor color = Green) const;
//赋予不同的缺省参数值,这是不好的行为。
...
};
class Circle:public Shape{
public:
virtual void draw(ShapeColor color) const;
//注意:当用该方法写时,当用户以对象调用此函数,一定要指定参数值
//因为静态绑定下,这个函数并不从其基类继承缺省参数值。
//但若以指针(或引用)调用此函数,可以不指定参数值,
//因为动态绑定下,这个函数会从其基类继承缺省的参数值。
...
};
该继承体系图例如下:
--------- Rectangle
|
|
|
Shape <------
|
|
|
--------- Circle
考虑以下指针:
Shape* ps; //静态类型为Shape*
Shape* pc = new Circle; //静态类型为Shape*
Shape* pr = new Rectangle; //静态类型为Shape*
本例中,ps、pc和pr都被声明为pointer-to-Shape类型,所以它们都以它为静态类型,注意,不论它们真正指向声明,它们的静态类型都是Shape*。
对象的动态类型(dynamic type)则是指“目前所指对象的类型”。即动态类型可以表示出一个对象将会有声明行为。以上例而言,pc的动态类型为Circle*,pr的动态类型为Rectangle*。ps没有动态类型,因为它尚未指向任何对象。
动态类型一如其名称所言,可在程序执行过程中改变(通常是经由赋值动作):
ps = pc;//ps的动态类型如今是Circle*
ps = pr;//ps的动态类型如今是Rectangle*
虚函数是动态绑定而来,即调用一个虚函数时,究竟调用哪一份函数实现代码,取决于发出调用的那个对象的动态类型:
pc->draw(Shape::Red);//调用Circle::draw(Shape::Red);
pc->draw(Shape::Red);//调用Rectangle::draw(Shape::Red);
在带有缺省参数值的虚函数中,因为虚函数是动态绑定的,而缺省参数值却是静态绑定的,意思是你可能会在“调用一个定义于派生类里的虚函数”的同时,却使用基类为它所指定的缺省参数值:
pr->draw(); //调用Rectangle::draw(Shape::Red)
此例中,pr的动态类型是 Rectangle*,所以调用的是Rectangle的虚函数,这都没啥问题。Rectangle::draw函数的缺省参数值应该是GREEN,但由于pr的静态理性是Shape*,所以这一调用的的缺省参数来自基类Shape而非Rectangle类!所以这一函数调用有着奇怪且没人预料到的组合,由Shape类和Rectangle类的draw声明式各出一半力。
上述问题不仅局限在“ps、pc和pr都是指针”的情况,即使把指针换成引用(references)问题仍然存在。重点在于draw是个虚函数,而他有个缺省值在派生类中被重新定义了。
为什么C++要这么运作呢? 就是因为运行期效率。若缺省参数值是动态绑定的,编译器就必须有某种方法在运行期为虚函数决定适当的参数缺省值。这比目前实行的“在编译期决定”的机制更慢且更复杂。
当遵守这条规则,并同时提供缺省参数值给基类和派生类的用户,会怎样呢?
class shape{
pulic:
enum ShapeColor{Red,Green,Blue};
virtual void draw(ShapeColor color = Red) const = 0;
...
};
class Rectangle:public Shape{
public:
virtual void draw(ShapeColor color = Red) const;
...
}
太糟糕了,代码重复,更让人无法忍受的是又带有相依性(with dependencies):若Shape内的缺省参数值改变了,所有“重复给定缺省参数值”的哪些派生类也必须改变,否则他们最终会导致“重复定义一个继承而来的缺省参数值”。这个问题该如何解决呢?
即想让虚函数表现出你所想要的行为,有一种方法是使用虚函数的替代设计,其中之一是NVI(non-virtual interface)手法:让基类里的一个公有非虚函数调用私有虚函数,后者可被派生类重新定义。在这里我们可以让非虚函数指定缺省参数,而私有虚函数负责真正的工作:
class Shape{
public:
enum ShapeColor{Red,Green,Blue};
void draw(ShapeColor color = Red) const //非虚函数
{
doDraw(color); //调用一个虚函数
}
...
private:
virtual void doDraw(shapeColor color) const = 0;//真正
//的工作在此进行
};
class Rectanglre:public Shape{
public:
...
private:
virtual void doDraw(ShapeColor color) const;//在这里
// 不需要指定缺省参数值
...
};
由于非虚函数应该不被派生类覆盖,这个设计就使得draw函数的color缺省参数值总是为Red。
注意:绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定的,而虚函数(你唯一应该覆写的东西)却是冬天绑定的。