Effective C++手册Item 37: 绝不要重定义一个函数的 inherited default parameter value(通过继承得到的缺省参数值)

Item 37: 绝不要重定义一个函数的 inherited default parameter value(通过继承得到的缺省参数值)

作者:Scott Meyers

译者:fatalerror99 (iTePub's Nirvana)

发布:http://blog.csdn.net/fatalerror99/

我们直接着手简化这个话题。只有两种函数能被你 inherit(继承):virtual(虚拟的)和 non-virtual(非虚拟的)。然而,重定义一个 inherited non-virtual function(通过继承得到的非虚拟函数)永远都是一个错误(参见 Item 36),所以我们可以安全地将我们的讨论限制在你继承了一个 virtual function with a default parameter value(带有一个缺省参数值的虚拟函数)的情形。

在这种情况下,本 Item 的理由就变得非常地直截了当:virtual functions(虚拟函数)是 dynamically bound(动态绑定),而 default parameter values(缺省参数值)是 statically bound(静态绑定)。

那又怎样呢?你说 static(静态)和 dynamic binding(动态绑定)之间的区别早已塞入你负担过重的头脑?(不要忘了,static binding(静态绑定)也以 early binding(前期绑定)闻名,而 dynamic binding(动态绑定)也以 late binding(后期绑定)闻名。)那么,我们就再来回顾一下。

一个 object(对象)的 static type(静态类型)就是你在程序文本中声明给它的 type(类型)。考虑这个 class hierarchy(类继承体系):

// 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 = 0;
  ...
};
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;
  ...
};

直观地看,它看起来就像这个样子:

现在考虑这些 pointers(指针):

Shape *ps;                       // static type = Shape*
Shape *pc = new Circle;          // static type = Shape*
Shape *pr = new Rectangle;       // static type = Shape*

在本例中,ps,pc 和 pr 全被声明为 pointer-to-Shape 类型,所以它们全都以此作为它们的 static type(静态类型)。注意这就使得它们真正指向的东西完全没有区别——无论如何,它们的 static type(静态类型)都是 Shape*。

一个 object(对象)的 dynamic type(动态类型)取决于它当前引用的 object(对象)的 type(类型)。也就是说,它的 dynamic type(动态类型)表明它有怎样的行为。在上面的例子中,pc 的 dynamic type(动态类型)是 Circle*,而 pr 的 dynamic type(动态类型)是 Rectangle*。至于 ps,它没有一个实际的 dynamic type(动态类型),因为它(还)不能引用任何 object(对象)。

dynamic types(动态类型),就像它的名字所暗示的,能在程序运行中变化,特别是通过 assignments(赋值):

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

virtual functions(虚拟函数)是 dynamically bound(动态绑定),意味着被调用的特定函数取决于被用来调用它的那个 object(对象)的 dynamic type(动态类型):

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

我知道,这全是老生常谈;你的确已经理解了 virtual functions(虚拟函数)。但是,当你考虑 virtual functions with default parameter values(带有缺省参数值的虚拟函数)时,就全乱了套,因为,如上所述,virtual functions(虚拟函数)是 dynamically bound(动态绑定),但 default parameters(缺省参数)是 statically bound(静态绑定)。这就意味着你最终调用了一个定义在 derived class(派生类)中的 virtual function(虚拟函数)却使用了一个来自 base class(基类)的 default parameter value(缺省参数值)。

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

在此情况下,pr 的 dynamic type(动态类型)是 Rectangle*,所以正像你所希望的,Rectangle 的 virtual function(虚拟函数)被调用。在 Rectangle::draw 中,default parameter value(缺省参数值)是 Green。然而,因为 pr 的 static type(静态类型)是 Shape*,这个函数调用的 default parameter value(缺省参数值)是从 Shape class 中取得的,而不是 Rectangle class!导致的结果就是一个调用由“奇怪的和几乎完全出乎意料的 Shape 和 Rectangle 两个 classes(类)中的 draw 声明的混合物”所组成。

ps,pc,和 pr 是 pointers(指针)的事实与这个问题并无因果关系,如果它们是 references(引用),问题依然会存在。唯一重要的事情是 draw 是一个 virtual function(虚拟函数),而它的一个 default parameter values(缺省参数值)在一个 derived class(派生类)中被重定义。

为什么 C++ 要坚持按照这种不正常的方式动作?答案是为了运行时效率。如果 default parameter values(缺省参数值)是 dynamically bound(动态绑定),compilers(编译器)就必须提供一种方法在运行时确定 virtual functions(虚拟函数)的 parameters(参数)的 default value(s)(缺省值),这比目前在编译期确定它们的机制更慢而且更复杂。最终的决定偏向了速度和实现的简单这一边,而造成的结果就是你现在可以享受高效运行的乐趣,但是,如果你忘记留心本 Item 的建议,就会陷入困惑。

这样就很彻底而且完美了,但是看看如果你试图遵循本规则为 base(基类)和 derived classes(派生类)的用户提供同样的 default parameter values(缺省参数值)时会发生什么:

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;
  ...
};

噢,code duplication(代码重复)。code duplication(代码重复)带来 dependencies(依赖关系):如果 Shape 中的 default parameter values(缺省参数值)发生变化,所有重复了它的 derived classes(派生类)必须同时变化。否则它们就陷入重定义一个 inherited default parameter value(通过继承得到的缺省参数值)。怎么办呢?

当你要一个 virtual function(虚拟函数)按照你希望的方式运行有困难的时候,考虑可选的替代设计是很明智的,而且 Item 35 给出了多个 virtual function(虚拟函数)的替代方法。替代方法之一是 non-virtual interface idiom (NVI idiom)(非虚拟接口惯用法):用 base class(基类)中的 public non-virtual function(公有非虚拟函数)调用 derived classes(派生类)可能重定义的 private virtual function(私有虚拟函数)。这里,我们用 non-virtual function(非虚拟函数)指定 default parameter(缺省参数),同时使用 virtual function(虚拟函数)做实际的工作:

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 = 0;  // 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.
};

因为 non-virtual functions(非虚拟函数)绝不应该被 derived classes(派生类) overridden(覆盖)(参见 Item 36),这个设计使得 draw 的 color parameter(参数)的 default value(缺省值)应该永远是 Red 变得明确。

Things to Remember

绝不要重定义一个 inherited default parameter value(通过继承得到的缺省参数值),因为 default parameter value(缺省参数值)是 statically bound(静态绑定),而 virtual functions ——应该是你可以 overriding(覆盖)的仅有的函数——是 dynamically bound(动态绑定)。