Effective C++条款37:绝不重新定义继承而来的缺省参数值(Never redefine a function‘s inherited default parameter value)

Effective C++条款37:绝不重新定义继承而来的缺省参数值(Never redefine a function's inherited default parameter value)


《Effective C++》是一本轻薄短小的高密度的“专家经验积累”。本系列就是对Effective C++进行通读:

第6章:继承与面向对象设计

在这里插入图片描述


条款37:绝不重新定义继承而来的缺省参数值

  你只能继承两种函数:虚函数和非虚函数。然而,重新定义一个继承而来的非虚函数永远是错误的(见条款36),所以我们可以安全的把这个条款的讨论限定在“继承一个带有缺省参数值的虚函数”。

  这个条款成立的理由非常直接而明确:虚函数是动态绑定,而缺省参数值是静态绑定的。

1、虚函数是动态绑定,而缺省参数值是静态绑定

  对象的所谓静态类型(static type),就是它在程序中被声明时所采用的类型,考虑如下的类继承体系:

// 一个用以描述几何形状的calss
class Shape {
    
    
public:
	enum ShapeColor {
    
     Red, Green, Blue };
	//所有形状都必须提供一个函数,用来绘出自己
	virtual void draw(ShapeColor color = Red) const = 0;
	...
};

class Rectangle: public Shape {
    
    
public:
	//注意,以上这么写则当客户以对象调用此函数,一定要指定参数值。
	//因为静态绑定下这个函数并不从其base继承缺省参数值
	//但若以指针(或reference)调用此函数,可以不指定参数值。
	virtual void draw(ShapeColor color = Green) const;
	...
};
class Circle: public Shape {
    
    
public:
	virtual void draw(ShapeColor color) const;
	...
};

  画成类继承图会是下面这个样子:

在这里插入图片描述

  现在考虑三个指针:

Shape *ps;                               
Shape *pc = new Circle;          
Shape *pr = new Rectangle;    

  在此例子中,ps、pc、pr都被声明为指向shape的指针,所以它们以Shape作为它们的静态类型。注意无论shape指针真正指向的是什么对象,静态类型都是Shape*

  对象的所谓的动态类型则是指“目前所指对象的类型”。也就是,动态类型可以表现出一个对象将会有什么行为。见上面的例子,pc的动态类型是Circle*,pr的动态类型是Rectangle*。对于ps,它实际上没有动态类型,因为它还没有引用任何对象。

  动态类型一如其名称所示,可在程序执行过程中改变(通常是经由赋值动作):

ps = pc;      
ps = pr;     

  虚函数是动态绑定的,意味着哪个函数被调用是由发出调用的对象的动态类型来决定的:

pc->draw(Shape::Red);     
pr->draw(Shape::Red);         

  如果你已经了解虚函数。但是,当你考虑带缺省参数值的虚函数时,麻烦出现了,因为虚函数是动态绑定的,但是默认参数是静态绑定的。这意味着你可能会终止一个虚函数的调用,因为函数定义在派生类中却使用了基类中的默认参数:

pr->draw();                          

  此例中,pr的动态类型是Rectangle*,所以Rectangle的虚函数被调用,这也是你所期望的。在Rectangle::draw中,默认参数值是Green。然而因为pr的静态类型是Shape*,这个函数调用的默认参数值是来自于Shape类而不是Rectangle类!最后的结果是这个调用由一个奇怪的也几乎是你意料不到的组合组成:也即是Shape类和Rectangle类中的draw声明混合而成。

  以上的事实不只是局限于“ps、pc、pr”都是指针的情况。如果它们是引用也同样会出现这个问题。唯一重要的事情是draw是一个虚函数,并且默认参数中的一个在派生类中被重新定义了。

读到这里,有人可能就会产生一些疑问?为什么C++坚持以这种方式来运行?

  答案和运行时效率相关。如果一个默认参数是动态绑定的,编译器就需要用一种方法在运行时为虚函数参数确定一个合适的默认值,比起当前在编译期决定这些参数的机制,它更慢更加复杂。做出的决定是更多的考虑了速度和实现的简单性,结果是你可以享受高效的执行速度,但是如果你没有注意到这个条款的建议,你就会很迷惑。

2、为基类和派生类提供相同的默认参数

  这都很好,但如果你试着遵循这条规则,并且同时提供缺省参数值给基类和派生类的用户,会发生什么呢?

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 = Red) const;
	...
};

  代码重复的问题出现了。更糟的是,与代码重复问题便随而来的代码依赖问题:如果Shape中的默认参数被修改了,所有“重复给定缺省参值”的派生类都需要被修改。否则它们最终会导致“重新定义一个继承而来的缺省参数值”。咋办?

  当你让虚函数按照你的方式来运行时遇到了麻烦,考虑替代设计方法是很聪明的,条款35中介绍了替换虚函数的不同方法。其中的一个是非虚接口用法(NVI idiom):用基类中的public非虚函数调用一个private虚函数,private虚函数可以在派生类中重新被定义。现在,我们用非虚函数指定默认参数,而用虚函数来做实际的工作:

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 Rectangle: public Shape {
    
    
public:
	...
private:
	virtual void doDraw(ShapeColor color) const; 
	...                                                               
};          

  由于非虚函数应该绝对不会在派生类中被重定义(条款36),这个设计保证draw的color缺省值参数总是为Red。

3、牢记

  • 绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virutal函数——你唯一应该覆盖的东西——却是动态绑定。

总结

期待大家和我交流,留言或者私信,一起学习,一起进步!

猜你喜欢

转载自blog.csdn.net/CltCj/article/details/128661294