[声明]:本英文资料源自于Herb Sutter 创建的“Conversation”栏目,“C++ 翻译小组”的翻译作品供学习交流与参考用途,不得用于任何商业用途。未经Herb Sutter、Jim Hyslop同意,不得转载;对于违反以上条款,翻译小组对此不负任何责任;特此声明。
文章来源:http://www.gotw.ca
版权归属:Herb Sutter and Jim Hyslop
译 者:myheartforver
对话#06:Virtually Yours
我又拧了一下扳手 ,但毫无用处,我应该试着用锤子猛敲它 ,但那看起来不是个好主意。“它一动不动”,我说道,“剩余的空间足够再放一个模块,但原先那些零件固定得很紧,动不了。”
机器只允许一个人操作,珍妮只能旁观,她忍不住抱怨:“,唉,真是的,往扳手上添个零件就这么困难么?”
“如果零件没有正确组合在一起的话,就可以”,我叹道,“那些制造的人只需要考虑东西能用就可以了,里面的东西都紧紧的合在一起了,行,我们可以添加零件,但是首先,我们不得不把它先拆开来,然后再重装好。”
“欢迎到木卫二, 自身体重牵引系统中心,回家后我们就拥有这项技术了,可以自己试试了。”
我摸了摸额头,“你知道”,我沉思一会说到,“这让我想起...”,珍妮笑了,这表明我们可以休息一下喝杯咖啡了,我开始告诉她另一个关于我第一份工作的故事。
- - - - - - - - - - - - - - - - - - - - - - - - -
我正在为一个设计方案编写代码,该方案是住我隔壁的程序员温迪设计的。她为项目设计一个抽象类,而我的工作就是设计一个实体类继承她的抽象类。
就像往常一样,我通过公共接口来研究一个新的类:
class Mountie
{
public:
void read( std::istream & );
void write( std::ostream & ) const;
virtual ~Mountie();
};
毫不惊奇—析构函数设计成虚拟函数,暗示这个类将作为基类使用;但用于读与写的成员函数却是非虚拟的,这时我想要看看其他代码:
所以我把目光移到类的保护接口上:
protected:
virtual void do_read( std::istream & );
virtual void do_write( std::ostream & ) const;
很快,我意识到温迪使用了模板方法模式(Template Method pattern):公共、非虚拟成员函数显然将调用受保护的虚拟函数[1]。我对很快理解了这点感到洋洋自得,我做好了应付任何事情的准备。可当我看到私有接口时,我的这股得意劲消失了。
private:
virtual std::string classID() const = 0;
我停止了自得,一个纯虚拟私有函数?它怎么可能工作呢?我百思不得其解。
“嗯,温迪”我说,“你的mountie类无法工作。它有一个私有的虚拟函数。”
“你试过吗?”温迪问道,头也没从她的键盘上抬一下。
“噢,好吧,没有”,我承认,“但我的继承类无法覆盖classID这个函数”
“你如此肯定?”Guru安静的声音把我们都吓了一跳。“和你在一起总是什么事都干不成。难道这几个月来你什么都没学到,还是个菜鸟?你可是我的徒弟啊!
尽管Guru的语气很平静,但这几句咄咄逼人的话仍使我感到几分惶恐。
“我的孩子“,她继续说道,“你忘了存取权限和虚拟性是互相独立的,决定一个函数是静态绑定还是动态绑定取决于这个函数最后是如何被调用的。你需要好好读读神圣标准第三章4节第一行,和第五章2.2节第一行。
是时候展现我的水平了,我开口说:“噢,是的”,我决定使出另外一招---扯开话题。“我只是不理解为什么它是私有的。这看起来好像对我没什么意义。”
“仔细考虑一下这种情况,徒弟。你创建了一个类,再仔细考虑一下它的设计以后,你觉得这个类需要一个不能被任何其他类调用的成员函数,甚至是它的继承类,你会怎么做呢?”
“当然是申明为私有了”,我回答道。Guru看着我,她的眉毛皱了起来。我的大脑开始飞速的转了起来,拼命的想私有函数和虚拟函数之间有什么联系。
温迪说:“你检查过mountie类的实现没有,特别是write函数?”
我很快的转到我的键盘上,很高兴逃离Guru不松懈的注视。我很快就看到了它:
void Mountie::write(std::ostream &Dudley) const
{
Dudley << classID() << std::endl;
do_write(Dudley);
}
我应该告诉温迪我童年花了太多的时间看动画片了。“我现在明白了”,我说道“函数classID是一个实现细节,用来表示正在保存的类的具体类型。继承类必须重载这个函数,但既然它是个实现细节,那就让它为私有。”
“不错,小伙子”Guru点点头“现在解释给我听听为什么do_write和do_read不是私有的。”
这让我想了好一会儿,突然灵机一动:“因为继承类必须调用它父类这些函数的实现以使父类有机会读写继承类的数据。”
“很好,徒弟,”Guru几乎微笑了,“但为什么不让它们为公有?”
“因为,”我受到鼓舞继续说道,“它们必须以一种受控制的方式被调用,特别是do_write函数,对象类型不得不首先写到流中,使得当对象被读取时,对象的创建者得以知晓对象的类型以便从流中装载并创建对象。如果把这些函数设为公有,会导致混乱。
“非常聪明,徒弟。”Guru暂停了一会,“就像口语一样,c++语言除了语法和规则外还有很多东西需要了解,你必须要知道这些东西。
“对,Coplien的书是我下一本要读的书。”
Guru抬起手:“安静点,孩子。我不是说大师Coplien。我是说用词习惯的语言感觉---特定结构下的隐含意思。举例来说,你知道,一个虚拟析构函数将会告诉你我将会作为多态基类,你可以从我这里按你所需继承新类,但是一个非虚拟析构函数则告诉你我不会作为多态基类,请不要从我继承,我祈祷。”
“类似的,虚拟函数的存取权限表达了初始化的通常意义。一个保护的虚拟成员告诉你一些东西应该或可能甚至必须依赖于这个函数的实现。一个私有的虚拟函数表明某些东西可能或者不可能会重载我,这取决于他们的选择。然而他们可能不会调用我的实现 。”
当我领会到这点,我点点头 :“那么公共虚拟函数呢?”
“尽可能避免这样做,最好使用模板方法,考虑这种情况”,她拾起干擦笔,开始写了起来,字迹很细:
class HardToExtend
{
public:
virtual void f();
};
void HardToExtend::f()
{
// 执行专有的动作
}
“假设你发布了这个类,但不久,你的需求发生了变化”,她继续说道,“在这个类的第二个版本里,你发现在函数f()中需要实现模板方法(Template Method )。而这几乎不可能实现,你知道这是为什么?
“啊,是的,好吧...我认为...不,我不知道。“
“有两个可能的途径让这个类使用模板方法。第一个途径是把f()的实现代码移到一个新的函数然后让f()成为非虚拟的,就像这样:
class HardToExtend
{
// 可以保护
virtual void do_f();
public:
void f();
};
void HardToExtend::f()
{
// 前处理
do_f();
// 后处理
}
void HardToExtend::do_f()
{
// 执行专有的动作
}
“然而,HardToExtend’的继承类希望能覆盖f(),而不是do_f().你现在必须改变所有从HardToExtend继承而来的类。只要你漏掉了一个,这个类将试图覆盖一个非虚拟函数,这将会导致,用大师Meyers的话来说,在你的类继承体系中有不可预期的行为。
“另一个解决之道是引进非虚拟函数,然后把f()移到私有部分,像这样:”
class HardToExtend
{
// possibly protected
virtual void f();
public:
void call_f();
};
“这将会给类的使用者无休止的烦恼。所有HardToExtend的使用者都会试图调用f(),而不是call_f()。 HardToExtend的客户代码根本就无法编译。此外,它的继承类将很可能把f()放到公共接口里,然后使用这些继承类的客户代码会直接地,而不是通过HardToExtend的指针和引用,对你想保护的函数进行存取。
“虚拟函数应该和数据成员一样对待----让他们成为私有的,除非设计需求表明应该有较少的限制。提升它们到更高存取级别比把它们降到更私有的级别更容易些。”
- - - - - - - - - - - - - - - - - - - - - - - - -
“这些事是什么时候发生的呢?”珍妮问道。
“就在新千年来临之前---真正的来临,也就是说,我想是在2000年末。我们那时候即将庆贺新年,我们那时在展望2001年,因为所有的文化,你知道.. .”
很奇怪,这场谈话后没多久我们听说在木卫二地表下面发现了一个方尖塔。
[注释]
Notes
[1] E. Gamma, R. Helm, R. Johnson, and J. Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1995).
[1]E.Gamma,R,Helm,R.Johnson,and J.Vlissides.设计模式:
[2] ISO/IEC 14882:1998, "Programming Languages — C++," clauses 3.4 and 5.2.2.
[3] J. Coplien. Advanced C++ Programming Styles and Idioms (Addison-Wesley, 1992).
[4] S. Meyers. Effective C++: 50 Specific Ways to Improve Your Programs and Design, 2nd edition (Addison-Wesley, 1998); Item 37: "Never redefine an inherited non-virtual function."
[关于作者]
Herb Sutter
是个独立顾问,也是ISO/ANSI C++标准委员会的秘书。你可通过hsutter@acm.org.联系他
Jim Hyslop
Leitch Technology International Inc.资深的软件设计师,你可通过jim.hyslop@leitch.com联系他