The C++ Style Sweet Spot
羅翼 譯 蔣賢哲 校
從C往上
Bill Venners:在一次采訪中,您曾說(shuō)過(guò):“C++社群正在逐漸消化C++標(biāo)準(zhǔn)所提供的基礎(chǔ)設(shè)施。通過(guò)重新思考C++使用風(fēng)格,在代碼的編寫、正確性、可維護(hù)性以及效率上都可以得到很大改進(jìn)”。請(qǐng)問(wèn)C++程序員該如何重新思考C++的使用風(fēng)格呢?
Bjarne Stroustrup:通常情況下,指出不要做什么比指出要做什么容易得多,所以我也將采用這種方式來(lái)回答你的問(wèn)題。很多人認(rèn)為C++只不過(guò)是對(duì)C作了一些細(xì)小的擴(kuò)充而已。在他們的代碼中,充斥著原生指針和原生數(shù)組,他們將原先在C語(yǔ)言中使用malloc的地方換為使用new??偠灾?,他們的代碼的抽象層次很低。使用C形式的編碼風(fēng)格是一種使用C++的方式,可是這種方式并不能有效地利用C++。
我認(rèn)為一種較好的使用C++的方法就是采用標(biāo)準(zhǔn)庫(kù)提供的一些基礎(chǔ)設(shè)施,例如使用vector代替原生數(shù)組。vector知道自己的大小,而原生數(shù)組就做不到。你可以方便地隱式或顯示地改變一個(gè)vector的大小。如果需要改變一個(gè)原生數(shù)組的大小,你必須利用realloc、malloc以及memcpy等函數(shù)顯示處理內(nèi)存相關(guān)的問(wèn)題。再例如利用內(nèi)聯(lián)函數(shù)代替宏,你就可以避開一些與宏相關(guān)的問(wèn)題。還有,使用C++中的string類而不是顯示地去手工操縱一個(gè)C字符串。如果在你的代碼中出現(xiàn)了大量的轉(zhuǎn)型操作符,那么幾乎可以肯定代碼中存在某些問(wèn)題,因?yàn)槟阋呀?jīng)從一種較高的、基于類型的抽象層次下降到了一種低級(jí)的、直接與位和字節(jié)打交道的層次了。你不應(yīng)該縱容這種情況經(jīng)常發(fā)生。
脫離低抽象層次的風(fēng)格并不意味著你需要從頭手工打造一些基礎(chǔ)的類來(lái)開始你的工作,作為替代,你可以使用由類庫(kù)提供的設(shè)施。標(biāo)準(zhǔn)庫(kù)是最容易想到同時(shí)也最顯而易見的一個(gè)類庫(kù),當(dāng)然,同時(shí)也還存在著許多其它致力于不同專業(yè)領(lǐng)域的庫(kù),例如數(shù)學(xué)或系統(tǒng)程序設(shè)計(jì)。作為一個(gè)示例,你并不需要在C層次編寫你的線程代碼,你可以使用一個(gè)C++線程庫(kù),例如Boost.Threads,而且在C++世界,線程庫(kù)的數(shù)量是相當(dāng)多的。再例如如果你需要使用回調(diào)函數(shù)機(jī)制的話,你最好不要直接 采用原生的C函數(shù)風(fēng)格,而可以使用一個(gè)名為libsigc++的庫(kù)來(lái)替代,它將為你正確 地處理很多與回調(diào)機(jī)制相關(guān)的事務(wù),包括回調(diào)類、槽位和信號(hào)等。采用這種類庫(kù)是非常值得的,因?yàn)樗梢允鼓愕拇a更接近你的理想,并能使你從紛繁易錯(cuò)的細(xì)節(jié)中抽出身來(lái),集中精力解決主要問(wèn)題。
很多這樣的技術(shù)都曾受到過(guò)“效率低下”的不公指責(zé)。發(fā)出這種指責(zé)的基本假設(shè)就是“優(yōu)雅和高階就意味慢”。是的,我承認(rèn)在某些情況下會(huì)慢,那么我們應(yīng)該在較低的層面處理這些情況,但可以從較高的層面著手。在有些場(chǎng)合下,你不會(huì)有任何負(fù)擔(dān),例如vector就和原生數(shù)組一樣快。
面向?qū)ο蟮姆簽E
另一種使人們陷入困境的情況則恰恰相反:這些人認(rèn)為C++應(yīng)該是一種極其高階的語(yǔ)言,應(yīng)該一切面向?qū)ο?。他們?jiān)信每當(dāng)他們需要增加一個(gè)新功能時(shí),就需要在一個(gè)擁有很多虛函數(shù)的龐大的類繼承體系中插入一個(gè)新 類。這種思想已反映在諸如Java這樣的語(yǔ)言中,但很多問(wèn)題并不適合用類層次結(jié)構(gòu)來(lái)解決。例如一個(gè)整數(shù)就不應(yīng)該成為類繼承體系的一部分 ,這根本不需要,如果你強(qiáng)制性地將它加進(jìn)去,不僅會(huì)付出高昂的代價(jià),而且很難做到優(yōu)雅。
你可以只使用一些獨(dú)立的類來(lái)進(jìn)行程序設(shè)計(jì)。假如我需要一個(gè)復(fù)數(shù),我只需要一個(gè)代表復(fù)數(shù)的類就可以了,它沒有虛函數(shù),與繼承體系也沒有任何關(guān)系。繼承體系只有當(dāng)程序需要時(shí)才是必需的。以我的書中最古老的那個(gè)從Simula借鑒而來(lái)的shape例子為例,擁有一個(gè)shapes繼承體系或一個(gè)windows繼承體系之類的東西是有意義的 ,但在其他很多情況下,你并不需要設(shè)計(jì)一個(gè)繼承體系,因?yàn)槟愀静恍枰?
所以,你可以從一種簡(jiǎn)單得多的抽象開始。再一次,標(biāo)準(zhǔn)庫(kù)為我們提供了一些例子:vector、string和復(fù)數(shù)類。除非你確實(shí)需要,否則不要?jiǎng)佑美^承體系。再例如,如果在你的代碼中出現(xiàn)了很多從基類到派生類的轉(zhuǎn)型 運(yùn)算符,這很可能就是一個(gè)危險(xiǎn)的信號(hào),提醒你已經(jīng)在繼承體系中走得太遠(yuǎn)了。在“遠(yuǎn)古”C++的年代,這種轉(zhuǎn)型一般 是通過(guò)C風(fēng)格的轉(zhuǎn)型運(yùn)算符實(shí)現(xiàn)的,這是不安全的。而在更為“現(xiàn)代”的C++中,你已經(jīng)可以使用動(dòng)態(tài)轉(zhuǎn)型符來(lái)完成工作了,無(wú)論如何,至少這種轉(zhuǎn)型是安全的。在一個(gè)好的設(shè)計(jì)中,轉(zhuǎn)型應(yīng)該只出現(xiàn)在你從程序外部收集信息來(lái)產(chǎn)生對(duì)象時(shí),因?yàn)?那時(shí)對(duì)象類型是不確定的,你只有在稍后收集到完整信息后才能確定對(duì)象的正確類型并轉(zhuǎn)型之。
Bill Venners:在太過(guò)低層和太著迷于面向?qū)ο筮@兩條路上走下去會(huì)有什么樣的代價(jià)?問(wèn)題何在?
Bjarne Stroustrup:如果你用寫C代碼的思想來(lái)寫代碼,那你就會(huì)遇到C形式的問(wèn)題:緩沖區(qū)溢出、指針問(wèn)題、難以維護(hù)的代碼等等。因?yàn)槟愕某橄髮哟翁?,所以代價(jià)就是開發(fā)時(shí)間和維護(hù)時(shí)間的延長(zhǎng)。
再來(lái)看看龐大的繼承體系帶來(lái)的問(wèn)題。你需要寫更多的原本不需要的代碼,維護(hù)更多的各部分之間的聯(lián)系。我特別不喜歡有很多get-set函數(shù)的類,它給人的第一感覺更像是一個(gè)數(shù)據(jù)集合而不是一個(gè)類。如果它真的是一個(gè)數(shù)據(jù)集合,那就讓它回歸到數(shù)據(jù)集合罷。
類應(yīng)該強(qiáng)制執(zhí)行不變式
Bjarne Stroustrup:根據(jù)我的經(jīng)驗(yàn),當(dāng)且僅當(dāng)你確定你的類中有一個(gè)不變式時(shí),你應(yīng)該設(shè)計(jì)一個(gè)具有接口的類以及一個(gè)隱藏的表示。
Bill Venners:您所說(shuō)的不變式是什么意思?
Bjarne Stroustrup:是什么讓一個(gè)對(duì)象成為一個(gè)有效的對(duì)象?不變式允許你說(shuō)出一個(gè)對(duì)象的表示何時(shí)良好何時(shí)不好。以vector作為一個(gè)非常簡(jiǎn)單的例子,一個(gè)vector知道它容納了n個(gè)元素,它有一個(gè)指向這n個(gè)元素的指針。這兒不變式就是指指針指向一塊內(nèi)存區(qū)域,而這塊內(nèi)存區(qū)域可以容納n個(gè)元素。如果它容納了n-1或者n+1個(gè)元素,那就出現(xiàn)了bug。如果指針為0,那也是bug,因?yàn)樵撝羔槻⑽粗赶蛉魏螙|西,這就表示了它違背了一個(gè)不變式。所以你必須分辨出哪些對(duì)象有意義,哪些是好的,哪些是壞的。這樣你就可以提煉出維護(hù)不變式的接口。這是檢查成員函數(shù)合理性的一種途徑,同時(shí)也是判斷一個(gè)操作是否應(yīng)該成為成員函數(shù)的一種方式。那些不需要與內(nèi)部表示混在一起的操作最好被安排到類外。這樣一來(lái),你就可以得到一個(gè)整潔、小巧而容易理解和維護(hù)的接口。
Bill Venners:這就是說(shuō),不變式表示一個(gè)類存在的正當(dāng)性,因?yàn)轭惐旧沓袚?dān)起了維護(hù)該不變式的責(zé)任?
Bjarne Stroustrup:沒錯(cuò)。
Bill Venners:這樣說(shuō)來(lái),不變式就是類中各個(gè)不同的數(shù)據(jù)成員之間的一種關(guān)系?
Bjarne Stroustrup:是這樣的。如果每個(gè)數(shù)據(jù)成員都可以被賦予任何值,那就沒什么太大必要做成一個(gè)類。以一個(gè)簡(jiǎn)單的“名字―地址”數(shù)據(jù)結(jié)構(gòu)為例,如果任何字符串都是合法的名字,任何字符串都是合法的地址,那么它本質(zhì)上就是一個(gè)結(jié)構(gòu),而不是一個(gè)類,請(qǐng)使用struct關(guān)鍵字來(lái)聲明它。千萬(wàn)別把名字和地址作為私有數(shù)據(jù)成員隱藏起來(lái),然后再提供類似于get_address、set_address、get_name以及set_name這樣的成員函數(shù)來(lái)存取它們?;蛘吒愀獾?,提供一個(gè)擁有g(shù)et_name和set_name之類的虛函數(shù)的抽象基類,然后在一個(gè)派生類中 重寫它們,這種做法純粹是挖空心思而已,絕無(wú)必要。
Bill Venners:您的意思是因?yàn)槟切╊愑星覂H有一種具體的實(shí)現(xiàn),所以把它們聲明為class是不必要的 ??墒怯幸环N辯解認(rèn)為:如果你將數(shù)據(jù)成員存取操作封裝為函數(shù),那么你就可以靈活地改變這個(gè)類的具體實(shí)現(xiàn)方式了。
Bjarne Stroustrup:大部分情況下是這樣的,但有些實(shí)現(xiàn)是你不會(huì)去改變的:你并不會(huì)經(jīng)常去改變一個(gè)整數(shù)、或者一個(gè)點(diǎn)、或者一個(gè)復(fù)數(shù)等類的實(shí)現(xiàn)。如果真的 需要改變它們的話,你應(yīng)該在某個(gè)地方做好設(shè)計(jì)決策。
下一層次,當(dāng)你要從原始的數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)移到真正的類的時(shí)候,讓我們?cè)俅我悦?地址為例:你應(yīng)該不會(huì)將這個(gè)類命名為name_and_address吧?也許你可以把它稱為personnel_record或者mailing_address。在這個(gè)層次上,你認(rèn)為名字和地址都不僅僅只是字符串而已。也許你想把名字拆開為first name、middle name和last name來(lái)分別存儲(chǔ),或者你決定采用一種由你自己確定語(yǔ)義的字符串來(lái)存儲(chǔ)這三個(gè)部分,你也可以決定是否判斷地址的有效性,并根據(jù)這些性質(zhì)將字符串分為first address、second address、城市、洲、國(guó)家以及郵編 之類的東西。
當(dāng)開始進(jìn)行這樣的分解工作時(shí),你就應(yīng)該開始考慮更改這個(gè)類的具體實(shí)現(xiàn)的可能性了。這時(shí),你要開始做出決定:是真的往類中加入一個(gè)私有數(shù)據(jù)成員,并使用繼承 ,還是僅僅只使用一個(gè)平凡的類,并固定其表現(xiàn)形式,或者你希望為數(shù)據(jù)提供一個(gè)抽象接口,這樣它們就可以擁有不同的表現(xiàn)形式。這里的重點(diǎn)不是“如何決定”,而是“做出決定”。你不能毫無(wú)章法可言地將一些類和函數(shù)堆砌在一起 ,如果你決定采用私有數(shù)據(jù)成員的話,你需要先定義一些確切的語(yǔ)義。
整個(gè)事情的思路是:在構(gòu)造函數(shù)中,建立好成員函數(shù)進(jìn)行操作時(shí)所必需的環(huán)境。換句話來(lái)說(shuō),構(gòu)造函數(shù)建立了不變式。而為了建立不變式,你通常需要分配一些資源。在析構(gòu)函數(shù)中,你可以清理環(huán)境并切釋放資源 ,這些資源可以是內(nèi)存、文件、同步鎖以及socket連接等,凡是你能想到的,都可以在析構(gòu)函數(shù)中被釋放。
設(shè)計(jì)簡(jiǎn)單的接口
Bill Venners:您剛才提到不變式可以幫助我們確定什么東西是應(yīng)該放到接口 中的,可以解釋得更詳細(xì)一點(diǎn)嗎?我現(xiàn)在試著復(fù)述一遍您剛才所說(shuō)的概念,看看我是否已經(jīng)理解了它。第一點(diǎn),所有和不變式相關(guān)或能夠操作不變式的功能都應(yīng)該放到類中。
Bjarne Stroustrup:對(duì)!
Bill Venners:任何僅僅使用數(shù)據(jù) 而不會(huì)對(duì)不變式產(chǎn)生影響的功能就不需要放到類中。
Bjarne Stroustrup:我來(lái)給個(gè)例子具體說(shuō)明一下 ,有些操作是你一定要與具體實(shí)現(xiàn)進(jìn)行直接的交互才能完成的。如果一個(gè)操作會(huì)改變一個(gè)vector的大小,那你最好也讓 其同時(shí)改變vector內(nèi)容納的元素的數(shù)量。如果你僅僅需要讀出這個(gè)大小變量的話,那么肯定應(yīng)該存在這樣的一個(gè)成員函數(shù)。但除 了這些需要直接與不變式打交道的基本函數(shù)外,還存在許多以這些函數(shù)為基礎(chǔ)的其他函數(shù),比如對(duì)vector的高效存取、查找和搜索操作等 ,這些函數(shù)就最好不要被設(shè)計(jì)為成員函數(shù)。
作為另一個(gè)例子,讓我們?cè)賮?lái)看看日期類。所有能夠改變年、月、日的操作應(yīng)該被作為該類的成員函數(shù),而那些諸如搜索下一個(gè)周末、周日的函數(shù)則可以建構(gòu)在基本成員函數(shù)之上。我曾經(jīng) 看到過(guò)一個(gè)擁有60到70個(gè)成員函數(shù)的日期類,那個(gè)類的設(shè)計(jì)者把所有操作都放到類里面去了,甚至包括find_next_Sunday這樣的函數(shù)也不例外,實(shí)際上這些函數(shù)在邏輯上與這個(gè)類沒有任何關(guān)系,如果你將這個(gè)函數(shù)作為該類的成員變量,那么這些函數(shù)就可以接觸到類中的具體數(shù)據(jù)成員,這就意味著如果你想改變?nèi)掌诘谋憩F(xiàn)形式的話,你需要 修改近60個(gè)函數(shù),在近60個(gè)地方修改 (譯注:可能還不止:))。
作為一種替代,如果你為這個(gè)日期類建立一個(gè)具有相互聯(lián)系的簡(jiǎn)單接口的話,那么出于邏輯相關(guān)或者性能等方面的考慮,這個(gè)類中應(yīng)該只會(huì)有5到10個(gè)左右的成員函數(shù) ― 雖然我想不出日期類有什么性能問(wèn)題可言,不過(guò)它的確是思考問(wèn)題時(shí)一個(gè)重要的焦點(diǎn)。然后以這5到10個(gè)基本操作為基礎(chǔ),你再把另外的50個(gè)操作放到一個(gè)支撐庫(kù)中。 最近這種思維方式被越來(lái)越多的人所接受。甚至在Java中,你也可以在擁有一個(gè)容器的同時(shí)擁有一個(gè)由靜態(tài)方法所構(gòu)成的支撐庫(kù)。
可惜的是,盡管我已經(jīng)為這種思想作了近20年的宣傳工作,人們?nèi)匀粌A向于把所有東西都放到類和繼承體系中去。關(guān)于上面提到的日期問(wèn)題,我還見過(guò)這樣的解決方案:提供一個(gè)基類,該基類有一些基本的操作和被聲明為protected的數(shù)據(jù)成員。日后當(dāng)你需要 添加一些新的工具函數(shù)時(shí),你需要從這個(gè)基類派生出一個(gè)新類,然后再在新類中加上新的工具函數(shù)。相信我,你的系統(tǒng)就是被這些東西弄得一團(tuán)糟的,把這些工具函數(shù)放到派生類中毫無(wú)道理可言。將這些工具函數(shù)分別獨(dú)立實(shí)現(xiàn)可以讓我們自由 地組合使用這些工具函數(shù)。由于你寫的函數(shù)和我寫的函數(shù)是完全獨(dú)立的,所以可以自由組合它們。如果我和你都從那個(gè)日期基類派生出新類型,然后通過(guò)往新類型中添加新函數(shù)的方法來(lái)實(shí)現(xiàn)各自的工具函數(shù),那么第三人將很難同時(shí)使用我們的函數(shù)庫(kù),在這里,類繼承體系 即被濫用了。
- 相關(guān)評(píng)論
- 我要評(píng)論
-