现在的位置: 首页 > 综合 > 正文

虚拟函数是否应该仅被声明为 private/protected?

2013年12月02日 ⁄ 综合 ⁄ 共 4555字 ⁄ 字号 评论关闭

 问题导入

         我想对于大家来说,虚拟函数并不能算是个陌生的概念吧。至于怎么样使用它,大部分人都会告诉我:通过在子类中重写(override)基类中的虚拟函数,就可以达到 OO 中的一个重要特性——多态 (polymorphism) 。 不错, 虚拟函数的作用也正是如此。 但如果我要你说一说虚拟函数被声明为 public和被声明为 private/protected
之间的区别的话,你又是否还能象先前一样肯定地告诉我答案呢?

         其实在一开始,我和大家一样,都喜欢把虚拟函数声明为 public(我并没有做太多的调查就说了这些,因为我身边的程序员们几乎都是这样做的) 。这样做的好处很明显:我们可以轻而易举地在客户端(client,相对于 server,server 指的是我们所使用的继承体系架构,client 指的就是调用该体系中方法/函数的外部代码)调用它,而不是通过利用那些烦人的
using 声明,或是强加给类的 friend关系来满足编译器的 access需求。OK,这是一个很不错的做法,简单、并且还能达到我们的要求。

         但根据 OO 三大特性中的另一个特性——封装(encapsulation)来说(另一个就是继承) ,需要我们将界面(interface)与实作(implementation)分开,即向外表现为一个通用的界面,而把实作上的细节封装在模块内不让 client 端知晓。界面与实作的分离,使得我们得以设计出耦合度更低、扩展性更好的系统来,并且还可以从这样的系统中提取出更多的可重用(reusable)的设计。 
         对于 OO 来说,封装是它的头等大事,享有最高的权利,其他的设计如果和它有着冲突,则以符合它的设计为准。这样,问题就出来了,万一我们所希望出现的多态正好是具体的实作细节并且我们不希望把它暴露给 client 端的话,那我们应该怎么样改动我们的设计以使得它能够适应封装的需求呢? 

 可行的解决办法

         幸好,C++中不但支持 public 的虚拟函数,也有着 private/protected 虚拟函数(在此我不想对于public和private/protected 之间的区别多说) 。前者是我们常用的形式,我也不多说,我们在此主要关心的是private/protected 的虚拟函数。
         你可能会有疑惑,既然虚拟函数被声明为 private(protected 不算,因为子类可以直接访问基类的protected成员) ,那子类中怎么还能对它进行重写呢?在此,你的疑虑是多余的,C++标准(也称ISO 14882)告诉我们,虚拟函数的重写与它的具体存储权限没有任何关系,即便是声明为 private的虚拟函数,在子类中我们也同样可以重写它因此,碰到上面所说的问题,我们就可以得到如下的设计:
 
class Base {
public:
 void do_something()
 {
  //......
  really_do_something();
  //......
 }
private:
  virtual void really_do_something()
 {
    //do the polymorphism code here
 }
};

 

class Derived: public Base {

private:
 void really_do_something()
 {
    //do the polymorphism code here
 }
}; 
 
        如果我们需要从上面的设计中得到实际上的多态行为,只要象下面一样调用 do_something就可以了:
 
//client code
Base& b;    //or Base* pb;
b.do_something(); //or pb->do_something();
 
       这样我们就得以解决了在开始处提出的那个问题。

 

问题引申

        那就这样完结了吗?没有。相反,至此我们才开始进行我们今天的讨论。首先让我们来看看多态的实现:
 
 void Base::do_something() {
  //......
  really_do_something();
  //......
 }
 
         我们可以发现,在调用真正对多态有贡献的 really_do_something()之前及调用后,我们还可以在其中添加我们自身的代码 (如一些控制代码等) , 这样我们 “好像” 就可以轻而易举地实现了Bertrand Meyers所提出的“Design By Contract”(DBC)1了:
 
void Base::do_something()
 {
    //our precondition code here
  really_do_something();
    //our postcondition code here
 }
 
         然后,让我们在去看看 Template Method这个Pattern2,发现所谓的 Template Method也主要就是通过这种方式来进行的。 于是, 我们是否可以这么想呢: 将所有的虚拟函数都声明为 private/protected,然后再使用一个public的非虚拟函数调用它,这样,我们不就得到了上面所列出的所有好处吗?

 

详细分析     
          简单看来,好像那么做真的是好处大大的,既不会造成效率上的损失(通过将该 public 的非虚拟函数inline化,简单的函数转调用的开销就可以被消除掉) ,又能够获得上述所有的好处。何乐而不为呢? 
 
          实际上来看,有不少程序员也正是这么做的(Herb Sutter 所调查的结果表明,这里面甚至还包括那些实作标准函数库的程序员们,当然,他们所考虑到的使用这种技巧的理由不会仅仅是我下面所给出的其他人的理由^_^) 。有的人甚至还认为,虚拟函数就应该被声明为 private/protected(当然,虚拟的析构函数不能够算在其中之列,否则就会有大乱子了) 。
 
          但让我们再仔细地考虑一下,想想一些比较极端的例子。假设我们有一个类,它拥有的虚拟函数的个数非常之多(就算它 10000 个吧) ,那即使大多数情况下只是简单的函数转调用动作,我们是否还应该为它的每一个虚拟函数都提供一个公开的非虚拟的界面呢?这时,为你的程序提供一个接口类(即没有任何成员变量,所有的方法都是纯虚函数的类)是一个不错的解决方案。

          还有,因为这样做的结果将会是:基类中的那个 public 的非虚拟界面函数必须能够适合所有的子类的情况,这样,我们将所有的责任都推倒基类上去了,这不能算是一个好的设计方法。假设我们有了一个继承体系极深的架构,在对基类进行了多次继承后,我们突然发现,新的子类已经无法适应原有的那个界面了。于是,为了继续执行我们的虚拟函数 private化,我们就将不得不把基类的代码给翻出来并改正它。幸运点的是,基类的代码是我们可以得到的,这样我们最起码还是有机会改正的(虽然有的时候,我们已经无法看懂基类中的代码了)
;糟糕的是,我们的基类是通过我们使用的一个函数库中得到的,而该函数库的代码我们无法获得,这个时候我们该怎么办呢?由此可见,如果在设计可能会被进行深度继承的类继承体系架构时,要想继续使用 private的虚拟函数的话,对于设计基类的要求就将会变的非常之高(因为在以后,基类的任何小小改动造成的后果传递到了继承的低端时都将被显著的放大) ,而让设计人员去猜测以后所有的可能使用情况是件不现实的事情,这样也就容易产生脆弱的、需要被频繁改动的设计。请记住一点:FBC(Fragile Base Class)是一件可怕的事情,在我们的程序中应当避免出现这种情况。
 
          另外,在你决定把你程序中的虚拟函数改为 private/protected 前,你有没有一个很好的理由呢?如果你只是说: “哦,我不知道,不过这样做可能会在以后的某天产生作用” 。不错,时刻让自己的程序保持可扩展性是很好的一件事情,但那都是基于你可以预见未来的扩展之上的(这种预见主要来自于你对于该领域的深刻认识或是你平时的经验)。在没有任何理由的情况下,仅仅靠着一句“它以后可能会有用”就往自己的程序中添加进去某种特性听起来好像很炫,但实际上它可能对你的程序有百害而无一利。在我们现有的各种
Framework 中,有着很多类似的“以后可能会有用”的特性,结果最终都被证明为没有被使用到,这不能不说是对于开发工作的一种浪费。因此,还是让我们记住在XP3中所说的YNGNI(You Never Going to Need It),对于现阶段没有用到的特性,还是不要提供为好。不过,如果你能够预见到以后的扩展的话,还是请你为它留下一个可扩展的便利。 

 
         此外,基于编译器的角度来看,当你一旦改动了基类,那么所需要重新编译的就不仅仅是基类本身了,所有从该基类继承下来的派生类也都将被重新编译。这样,我们就不得不又浪费掉大量的编译时间了。尤其是当我们决定大量使用 inline 的方式来转调用时,所需的时间就更加多了(因为inline函数在编译时会被扩展成实际的调用代码) 。这也可以算是一种语法上的FBC 问题。此外,当你决定向你的继承体系中增加一个函数,并改变了基类接口的行为,你就有可能破坏了整个继承体系,并使得外部的client端代码也受到了冲击。这种情况可以算是一种语义上的
FBC 问题。请记住:稳定的代码永远不要建立在不稳定的代码基础之上。
 
         现在,再让我们回到Template Method上面来看。什么时候该使用TM呢?从Design Patterns中得到它的意图为:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。Template Method使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。这和我们所谈论的虚拟函数是不是应该为private/protected完全是不相干的,虽说在实现TM时我们会用到private/protected的虚拟函数,但并不是所有的private/protected virtual都为TM。
 
         最后,完全使用 private/protected virtual 还有一个问题就是:OO 中所提倡的弹性。我们知道,OO 中的弹性通常都是由继承中的多态提供的, 但有时我们也会使用组合中的委托。 实际上已经有很多的 Patterns 都是这么做的了,如:Proxy, Adapter, Bridge, Decorator 等。如果一味地追求private/protected virtual, 势必使得我们只能在程序中使用继承了, 为了一棵树而放弃一片森林的事情,我想大家也都不愿意做吧。

 

结论  

         说了半天,我也该收工了:-)现在开始进行我观点的归纳: 一般说来,把虚拟函数声明为 private/protected 是一个很不错的设计方法4,但如果一旦把它作为一个唯一的 Silver Bullet来使用的话,就会产生许许多多的问题。在这篇文章中我也只是大概的谈了其中的部分,还有其他的一部分内容由于现今还没有完全整理好,也就不多说了。希望能够在下次再把它完善掉。

【上篇】
【下篇】

抱歉!评论已关闭.