条款37: 绝不重新定义继承而来的缺省参数值
Never redefine a function's inherited default parameter value先将讨论简化, 只能继承两种函数:virtual 和non-virtual 函数.然而 重新定义一个继承而来的non-virtual 函数永远是错误的(详见 条款36),所以可以安全地 将本条款的讨论局限于"继承一个带有缺省参数值的virtual函数".
这种情况下,本条款成立的理由就非常直接而明确了:virtual 函数系动态绑定,而缺省参数值却是静态绑定.
对象的所谓静态类型,就是它在程序中被声明时所采用的类型,考虑以下的 class 继承体系:
// 一个用以描述几何形状的class
class Shape {
public:
enum ShapeColor { Red, Green, Blue };
// 所有形状都必须提供一个函数,用来绘出自己
virtual void draw(ShapeColor color = Red) const = 0;
...
};
class Rectangle : public Shape {
public:
// 注意,赋予不同的缺省值
virtual void draw(ShapeColor color = Green) const;
...
};
public Circle : public Shape {
public:
virtual void draw(ShapeColor color) const;
// 注意,以上这么写,则当客户已对象调用此函数,一定要指定参数值.
// 因为静态绑定下这个函数并不从其base继承缺省参数值
// 但若以指针调用此函数,可以不指定参数值
// 因为动态绑定下这个函数会从其base继承缺省参数值
...
};
考虑这些指针:
Shape *ps;
Shape *pc = new Circle;
Shape *pr = new Rectangle;
本例中ps,pc,pr都被声明为pointer-to-Shape类型,所以它们都以它为静态类型,不论它们真正指向什么,它们的静态类型都是Shape*.对象的所谓动态类型则是指"目前所指对象的类型".也就是说,动态类型可以表现出一个对象将会有什么行为.以上例而言,pc的动态类型是Circle*,pr的动态类型是Rectangle*.
动态类型如其名称所示,可在程序执行过程中改变(通常是经由赋值动作):
ps = pc;
ps = pr;
virtual 函数系动态绑定而来,意思是调用一个 virtual 函数时,究竟调用哪一份函数实现代码,
取决于发出调用的那个对象的动态类型:
pc->draw(Shape::Red); // 调用Circle::draw(Shape::Red)
pr->draw(Shape::Red); // 调用Rectangle::draw(Shape::Red)
当考虑带有缺省参数值的 virtual 函数.virtual 函数是动态绑定,而缺省参数确实静态绑定.因此可能会在"调用一个定义于derived class内的virtual函数"的同时,却使用base class 为它所指定的缺省参数值:
pr->draw(); // 调用Rectangle::draw(Shape::Red)
此例中,pr的动态类型是Rectangle*,所以调用的是Rectangle的 virtual 函数.Rectangle::draw函数的缺省参数应该是GREEN,但由于pr的静态类型是Shape*,所以此调用的缺省参数值来自Shape class 而非Rectangle class .
结局是这个函数调用者奇怪并且几乎绝对没有人预料得到的组合,由Shape class 和Rectangle class 的draw声明式各出一半力.
重点在于draw是个 virtual 函数,而它有个缺省参数值在derived class 中被重定义了.
为什么C++坚持以这种乖张的方式运作呢?答案在于 运行期效率.如果缺省参数值是动态绑定,编译器就必须有某种办法在运行期为 virtual 函数决定适当参数缺省值.这比目前实行的"在编译器决定"的机制更慢且更复杂.为了程序的执行速度和编译器实现上的简易度,C++做了这样的取舍,其结果就是如今的执行效率.
如果遵守这条规则,并且同时提供缺省参数值给base和derived class 的用户,又会发生什么事?
class Shape {
public:
enum ShapeColor { Red, Green, Blue };
virtual void draw(ShapeColor color = Red) const = 0;
...
};
class Rectangle : public Shape {
pubic:
virtual void draw(ShapeColor color = Red) const;
...
};
代码重复,更糟糕的是,代码重复又带着相依性:如果Shape内的缺省参数值改变了,所有"重复给定缺省数值"的那些derived class 也必须改变,否则它们最终会导致"重复定义一个继承而来的缺省参数值".怎么办?
当想要 virtual 函数表现出想要的行为但遭遇麻烦,聪明的做法是考虑替代设计. 条款35列出了不少 virtual 函数的替代设计,其中之一是NVI(non-virtual interface)手法:令base class 内的一个 public non-virtual 函数调用 private virtual 函数,后者可被derived class 重新定义.这里可以让non-virtual 函数指定缺省参数,而 private virtual 函数负责真正的工作:
class Shape {
public:
enum ShapeColor { Red, Green, Blue };
void draw(ShapeColor color = Red) const { // 如今它是non-virtual
doDraw(color); // 调用一个virtual
}
...
private:
virtual void doDraw(ShapeColor color) const = 0; // 真正的工作在此处完成
};
class Rectangle : public Shape {
public:
...
private:
virtual void doDraw(ShapeColor color) const;
...
};
由于non-virtual 函数应该绝对不被derived class 覆写(详见条款36),这个设计很清楚地使得draw函数的color缺省参数总是Red.
注意:
绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而 virtual 函数——唯一应该覆写的东西——却是动态绑定.