虚拟函数是否应该被声明仅为private/protected?
問題導入?
我想對于大家來說,虛擬函數并不能算是個陌生的概念吧。至于怎么樣使用它,大部分人都會告訴我:通過在子類中重寫(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 } }; - 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
如果我們需要從上面的設計中得到實際上的多態行為,只要象下面一樣調用do_something就可以了:
//client code
Base& b; //or Base* pb;
b.do_something(); //or pb->do_something(); - 1
- 2
- 3
- 1
- 2
- 3
這樣我們就得以解決了在開始處提出的那個問題。
問題引申?
那就這樣完結了嗎?沒有。相反,至此我們才開始進行我們今天的討論。首先讓我們來看看多態的實現:
void Base::do_something(){//......really_do_something();//......} - 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
我們可以發現,在調用真正對多態有貢獻的really_do_something()之前及調用后,我們還可以在其中添加我們自身的代碼(如一些控制代碼等),這樣我們“好像”就可以輕而易舉地實現了Bertrand Meyers所提出的“Design By Contract”(DBC)[1]了:
void Base::do_something(){//our precondition code herereally_do_something();//our postcondition code here} - 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
然后,讓我們在去看看Template Method這個Pattern[2],發現所謂的Template Method也主要就是通過這種方式來進行的。于是,我們是否可以這么想呢:將所有的虛擬函數都聲明為private/protected,然后再使用一個public的非虛擬函數調用它,這樣,我們不就得到了上面所列出的所有好處嗎?
詳細分析?
簡單看來,好像那么做真的是好處大大的,既不會造成效率上的損失(通過將該public的非虛擬函數inline化,簡單的函數轉調用的開銷就可以被消除掉),又能夠獲得上述所有的好處。何樂而不為呢??
實際上來看,有不少程序員也正是這么做的(Herb Sutter所調查的結果表明,這里面甚至還包括那些實作標準函數庫的程序員們,當然,他們所考慮到的使用這種技巧的理由不會僅僅是我下面所給出的其他人的理由^_^)。有的人甚至還認為,虛擬函數就應該被聲明為private/protected(當然,虛擬的析構函數不能夠算在其中之列,否則就會有大亂子了)。?
但讓我們再仔細地考慮一下,想想一些比較極端的例子。假設我們有一個類,它擁有的虛擬函數的個數非常之多(就算它10000個吧),那即使大多數情況下只是簡單的函數轉調用動作,我們是否還應該為它的每一個虛擬函數都提供一個公開的非虛擬的界面呢?這時,為你的程序提供一個接口類(即沒有任何成員變量,所有的方法都是純虛函數的類)是一個不錯的解決方案。?
還有,因為這樣做的結果將會是:基類中的那個public的非虛擬界面函數必須能夠適合所有的子類的情況,這樣,我們將所有的責任都推倒基類上去了,這不能算是一個好的設計方法。假設我們有了一個繼承體系極深的架構,在對基類進行了多次繼承后,我們突然發現,新的子類已經無法適應原有的那個界面了。于是,為了繼續執行我們的虛擬函數private化,我們就將不得不把基類的代碼給翻出來并改正它。幸運點的是,基類的代碼是我們可以得到的,這樣我們最起碼還是有機會改正的(雖然有的時候,我們已經無法看懂基類中的代碼了);糟糕的是,我們的基類是通過我們使用的一個函數庫中得到的,而該函數庫的代碼我們無法獲得,這個時候我們該怎么辦呢?由此可見,如果在設計可能會被進行深度繼承的類繼承體系架構時,要想繼續使用private的虛擬函數的話,對于設計基類的要求就將會變的非常之高(因為在以后,基類的任何小小改動造成的后果傳遞到了繼承的低端時都將被顯著的放大),而讓設計人員去猜測以后所有的可能使用情況是件不現實的事情,這樣也就容易產生脆弱的、需要被頻繁改動的設計。請記住一點:FBC(Fragile Base Class)是一件可怕的事情,在我們的程序中應當避免出現這種情況。?
另外,在你決定把你程序中的虛擬函數改為private/protected前,你有沒有一個很好的理由呢?如果你只是說:“哦,我不知道,不過這樣做可能會在以后的某天產生作用”。不錯,時刻讓自己的程序保持可擴展性是很好的一件事情,但那都是基于你可以預見未來的擴展之上的(這種預見主要來自于你對于該領域的深刻認識或是你平時的經驗)。在沒有任何理由的情況下,僅僅靠著一句“它以后可能會有用”就往自己的程序中添加進去某種特性聽起來好像很炫,但實際上它可能對你的程序有百害而無一利。在我們現有的各種Framework中,有著很多類似的“以后可能會有用”的特性,結果最終都被證明為沒有被使用到,這不能不說是對于開發工作的一種浪費。因此,還是讓我們記住在XP[3]中所說的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],但如果一旦把它作為一個唯一的Sliver Bullet來使用的話,就會產生許許多多的問題。在這篇文章中我也只是大概的談了其中的部分,還有其他的一部分內容由于現今還沒有完全整理好,也就不多說了。希望能夠在下次再把它完善掉。
參考資料?
1、Object-Oriented Software Construction,Second Edition, Bertrand Meyer,清華大學出版社出版(影印版)?
2、設計模式可復用面向對象軟件的基礎, GoF, 李紅軍等譯,機械工業出版社出版?
3、Conversations: Virtually Yours, Herb Sutter & Jim Hyslop, CUJ?
以及網絡上相關的資料
后記?
寫該文的最初沖動來源于newsgroup: comp.lang.c++.moderated上面的一個討論:Virtual methods should only be private or protected?在觀看了Kevlin Henney,Herb Sutter以及James Kanze等幾位大師的精彩言論后,總想把自己的感受寫下來。在一開始,我倒是寫了很多,但沒有完全寫完。近來由于比較忙的情況,因此也就慢慢地把此事差點給忘記了。不是蟲蟲催著我要稿的話,我想也不知道要到什么時候我才能把它給寫完:-(,即便是現在,由于很久沒有復習這些資料,很多的東西也沒能寫進去,如果大家覺得意猶未盡的話,可以直接到newsgroup中找到該thread,里面有著完整的討論內容。
[1] 《Object-Oriented Software Construction》 Chapter 11:Design by Contract: building reliable software,國內有該書的影印版出售。?
[2] 《Design Patterns: Elements Of Reusable Object-Oriented Software》,國內有該書的中文翻譯版售?
[3] Extreme Programming,一種輕量級的軟件開發方式,注重開發中的靈活性,測試及其他……可以從下面網站上得到有關它的更多信息:www.extremeprogramming.org?
[4] 可以參見于Herb Sutter和Jim Hyslop發表的Conversations: Virtually Yours一文,在CUJ站點上可以找到這篇文章,此外,在csdn中也有過它的譯文。
轉載于:https://www.cnblogs.com/yzl050819/p/6844035.html
總結
以上是生活随笔為你收集整理的虚拟函数是否应该被声明仅为private/protected?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 荷花作者是谁啊?
- 下一篇: 《天真派武林外传》怎么只有18集?太短了