C++之虚函数的访问性

  在上次的一篇文章中,提到了private virtual函数,说实话直到当前自己所写的所有的虚函数都是public的,毕竟成员数据总应当被设置为private已经深入人心了,但是对成员函数的访问性貌似强调的不够多。后面网上搜了一下,虚函数的访问性还是挺有讲究的,顿时Sutter的两篇历史博文让自己醍醐灌顶,可见经典永流传啊。
  总体而言,涉及到虚函数应该秉持Non-Virtual Interface Idiom,相似的说法是Template Method涉及模式。

一、接口类型是non-virtual public,实现类型是virtual private

  如果一个成员还是是virtual public,那么这个函数就需要完成两个任务:定义调用接口、提供实现细节,而很多请看下这两个目标是对立竞争关系,因为接口要尽可能保持稳定,而实现要尽可能的方便修改更新。
  模板方法就是将接口定义为稳定的non-virtual,然后将实现和定制化的工作代理给private virtual成员函数,这样继承类就可以直接继承public函数作为稳定接口,同时override基类的private virtual进行定制化的实现。这样去做的话其好处有:
  (1) 在基类的公有接口中可以做很多pre-conditions和post-conditions的工作、插入度量性代码、写入调试跟踪日志等,跟一般的说是在调用之前设定好相关场景,而在调用之后清理相关场景,而不需要在每个派生类override的时候重复这一任务。
  (2) 接口和实现分类后,两者就不用像原本public virtual要实现一一对应的关系,比如在一个公共接口中可以按照一定的顺序、一定的条件可选择性的调用多个private virtual实现函数,派生类选择性的override某些或者全部虚函数,处理起来就更加灵活了。
  (3) 这样实现后的类后续修改和维护更加的方便,可以快捷的在public non-virtual接口中添加检查、调试等任何操作,派生类也可以按需独立的override业务部分,接口的使用者不受任何影响。
  (4) 关键的是这种手法几乎没有副作用,即使公有接口类没有额外的工作而仅仅当做一个函数wrapper,也可以使用inline进行可能的调用开销的优化。
  再次强调一次,函数的virtual和访问属性两者是正交独立的,即使基类的private virtual函数派生类不能访问,也不影响派生类对其override以提供定制化的行为实现。如果在派生类的代码中需要直接调用这个虚函数,那么这个虚函数可以为protected的,不过通常上面的Template Method都足够使用了,所以protected virtual也是很少见的情况。在Sutter统计标准库看来,non-public的虚函数占比达到95%以上,所以如果你的代码还有public virtual成员函数,看到这些就应该考虑改掉这些习惯了,否则是不是太不专业了。
  总的说,virtual成员函数应当被当做成员变量来看待——尽可能的让他们成为private。

二、析构函数要么virtual public,要么non-virtual protected

  这个的结论是:如果有多态析构对象的需求,那么析构函数就应该是public virtual的,否则就应该是protected non-virtual的。
  其实析构函数在很多教材中都被狠狠的过分被强调:如果发生了继承关系,那么基类的虚函数就应该是virtual的。这句话不完全正确,首先我们需要明确的是,如果在某个环境下可以析构某个对象,那么该对象的析构函数必须是可访问的,因此一个普通类要想直接构造器对象,那么该环境下其析构函数必须是公有的,否则编译器编译的时候直接报错,所以既然虚函数也像一般成员函数一样受到多态和访问控制限制,下面讨论起来就方便多了。
  如果允许通过基类的指针、引用进行对象的析构,那么就需要将基类的析构函数定以为public virtual的,因为根据虚函数的法则如果其指向的是派生类的对象,会调用派生类的析构函数,派生类的析构完成后会再调用基类的析构函数,对象就被完整析构了;如果不允许通过基类的指针、引用进行析构,那么基类的析构函数就可以为non-virtual protected类型,这样对象就只允许派生类的指针、引用以及派生类的实体对象进行直接析构,而派生类的析构必然会调用基类的析构函数,此时基类的析构函数就无须是virtual的了。
  这里说到的基类如果涉及到多态删除,那么必然是引用或者指针来操作的,那如果基类是一个非虚类(Concrete Class),而基类直接是对象析构而不涉及多态的话怎么论呢?各位C++大佬早就强调了:如果一个类要作为基类,那么就不要让他成为Concrete Class(不允许直接创建对象),反过来说就是不要派生Concrete Class,所以Meyers也生称:“Make non-leaf classes abstract”。
  如果还要更直白的表述,那么就是:如果基类B有一个派生类D,如果有可能用户使用B*指向实际的D类型的对象,而且可能会对这个指针执行delete类似的析构操作,则基类B需要一个virtual public析构函数。
  然后大师给出的理由是:析构函数作为第一个虚函数,如果添加了根本就用不到的多态功能,将带来所有运行时的开销,尤其当这个类很小(而且还没有其他的虚函数),那么自动强制一个虚析构函数就会显得显式增加了很多额外的开销。C++信奉的是高效率,“只为需要用到的东西付出代价”……

  所以,C++的确很复杂,即使摸熟了其语法和各种潜规则,要想真枪实弹的上战场,还需要各种Design Pattern和Idioms才能保驾护航,真的让人感到是“唯有套路得人心”啊~

本文完!

参考