读书笔记 effective c++ Item 37 永远不要重新定义继承而来的函数默认参数值

时间:2022-05-16 12:02:41

从一开始就让我们简化这次的讨论。你有两类你能够继承的函数:虚函数和非虚函数。然而,重新定义一个非虚函数总是错误的(Item 36),所以我们可以安全的把这个条款的讨论限定在继承带默认参数值的虚函数上。

1. 虚函数是动态绑定的,而默认参数是静态绑定的

在这种情况下,这个条款的验证就相当直接了:虚函数是动态绑定的,而默认参数值是静态绑定的。

这是什么?你说你不堪重负的脑袋已经忘记了动态绑定和静态绑定之间的区别?(为了好记,静态绑定也叫做早绑定(early binding),动态绑定也叫做晚绑定(late binding))。让我们看一下:

一个对象的静态类型是你已经在程序文本中声明的类型,考虑如下的类继承体系:

 // a class for geometric shapes
class Shape {
public:
enum ShapeColor { Red, Green, Blue };
// all shapes must offer a function to draw themselves
virtual void draw(ShapeColor color = Red) const = ;
...
}; class Rectangle: public Shape {
public:
// notice the different default parameter value — bad!
virtual void draw(ShapeColor color = Green) const;
...
};
class Circle: public Shape {
public:
virtual void draw(ShapeColor color) const;
...
};

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

读书笔记 effective c++ Item 37 永远不要重新定义继承而来的函数默认参数值

现在考虑三个指针:

 Shape *ps;                               // static type = Shape*

 Shape *pc = new Circle;           // static type = Shape*

 Shape *pr = new Rectangle;    // static type = Shape*

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

一个对象的动态类型由指针当前指向的对象类型来决定。也就是,它的动态类型表明了它的行为会是怎样的。看上面的例子,pc的动态类型是Circle*,pr的动态类型是Rectangle*。对于ps,它实际上没有动态类型,因为它还没有引用任何对象。

正如字面意思所表示的,在程序运行时动态类型是可以改变的,特别是通过赋值:

 ps = pc;      // ps’s dynamic type is now Circle*
ps = pr; // ps’s dynamic type is now Rectangle*

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

 pc->draw(Shape::Red);          // calls Circle::draw(Shape::Red)

 pr->draw(Shape::Red);          // calls Rectangle::draw(Shape::Red)

这些都是旧知识了,我知道你肯定了解虚函数。当你考虑带默认参数值的虚函数时,麻烦出现了,因为虚函数是动态绑定的,但是默认参数是静态绑定的。这意味着你可能会终止一个虚函数的调用,因为函数定义在派生类中却使用了基类中的默认参数:

  pr->draw();                           // calls Rectangle::draw(Shape::Red)!

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

Ps,pc和pr都为指针不是造成这个问题的原因。如果它们是引用也同样会出现这个问题。唯一重要的事情是draw是一个虚函数,并且默认参数中的一个在派生类中被重新定义了

2. C++为什么不对参数进行动态绑定?

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

3. 个例讨论——为基类和派生类提供相同的默认参数

这都很好,但是看看如果这么做会发生什么:遵守这个条款的规定并且为基类和派生类函数同时提供默认参数:

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

代码重复的问题出现了。更糟糕的是,与代码重复问题便随而来的代码依赖问题:如果Shape中的默认参数被修改了,所有重复这个参数的派生类都需要被修改。否则重新定义继承而来的默认参数值的问题会再度出现。该怎么做?

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

 class Shape {
public:
enum ShapeColor { Red, Green, Blue }; void draw(ShapeColor color = Red) const // now non-virtual { doDraw(color); // calls a virtual } ... private: virtual void doDraw(ShapeColor color) const = ; // the actual work is
}; // done in this func
class Rectangle: public Shape {
public:
...
private: virtual void doDraw(ShapeColor color) const; // note lack of a ... // default param val. };

因为非虚函数应该永远不会在派生类中被重定义(Item 36),这个设计保证draw的color默认参数应该永远是Red。

4. 总结

永远不要重新定义一个继承而来的默认参数值,因为默认参数值是静态绑定的,而虚函数——你应该重新定义的唯一的函数——是动态绑定的。