Imperfect C++ 中文版-經(jīng)過實踐檢驗的現(xiàn)實編程解決方案_第1頁
Imperfect C++ 中文版-經(jīng)過實踐檢驗的現(xiàn)實編程解決方案_第2頁
Imperfect C++ 中文版-經(jīng)過實踐檢驗的現(xiàn)實編程解決方案_第3頁
Imperfect C++ 中文版-經(jīng)過實踐檢驗的現(xiàn)實編程解決方案_第4頁
Imperfect C++ 中文版-經(jīng)過實踐檢驗的現(xiàn)實編程解決方案_第5頁
已閱讀5頁,還剩104頁未讀, 繼續(xù)免費閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報或認(rèn)領(lǐng)

文檔簡介

ImperfectC++中文版

——經(jīng)過實踐檢驗的現(xiàn)實編程解決方案

MatthewWilson著

榮耀劉未鵬譯

序言

—不完美主義實踐者的哲學(xué)

在本書中,C++語言技術(shù)與良好的實踐方式占有同等重要的地位.本書并非僅僅討論在某個特定場合下什么

方案才是有效的或技術(shù)上正確的,更重要的還是要看最終哪種做法才是更安全的或者更切合實際的。本書

要傳達(dá)的意思有四個方面:

原則1---C++是卓越的,但并不完美。

原則2—穿上“苦行衣”。

原則3—讓編譯器成為你的仆從。

原則4—永不言棄,總會有解決方案的。

這“四項基本原則”構(gòu)成了我所謂的“不完美主義實踐者”的哲學(xué)。

C++并不完美

多年以前,一位為她最小的兒子的過分驕傲的心理感到不安的母親曾這樣教導(dǎo)說:“如果你打算將一些好

的東西告訴別人,那么你最好也準(zhǔn)備承認(rèn)其中那些糟糕的成分?!敝x謝你,母親!

C++是一門杰出的語言。它支持高階概念,包括基于接口的設(shè)計、泛型、多態(tài)、自描述的軟件組件以及元編

程(meta-programming)等。此外,憑借對低階特性的支持,它在提供對計算機(jī)的精細(xì)控制方面,包括位

操作、指針以及聯(lián)合(union)等,也比大多數(shù)語言更有能耐。借助于這些范圍寬廣的能力,外加保持的對

高效性的根本性支持,C++可以說是當(dāng)今最杰出的通用編程語言[注口。不過話說回來,C++并非完美無瑕,

實際上遠(yuǎn)沒到完美的程度,因而有了本書的名字一“ImperfectC++”(中文版)。

[注1]請注意,我并不是說C++在所有特定問題領(lǐng)域都是最佳語言。例如,我絕不會建議你使用C++去編寫“專

家系統(tǒng)”,Prolog才是這方面的合適人選。而在系統(tǒng)腳本方面Pylhon和Ruby則當(dāng)仁不讓。此外,我們還得

承認(rèn)Java在企業(yè)級電子商務(wù)系統(tǒng)開發(fā)中確有過人之處。

由于一些很好的原因(有些是歷史原因,也有些是當(dāng)前的原因),C++不僅是一個折衷[Strol994]的產(chǎn)物,

而且還是一些互不相干、有時甚至是互不兼容的概念的混合體,因而其中必然存在著一些缺陷。有些缺陷

只是雞毛蒜皮,但有些就不是那么無關(guān)緊要了。許多缺陷都是從它們的“祖輩”那兒承襲而來的。其他則

是由于語言將效率放在高優(yōu)先級(幸好如此)才導(dǎo)致的。有些則可能是任何語言都無法擺脫的根本限制。

正是由于如今的語言變得愈來愈復(fù)雜,也愈來愈多種多樣,因而才出現(xiàn)了一些極有趣的問題,這些是任何

人都始料未及的。

本書直面這種復(fù)雜的形勢,堅信總能夠克服復(fù)雜性,堅信控制權(quán)最終還是掌握在那些見多識廣、經(jīng)驗豐富

的計算機(jī)專家手中。我的口標(biāo)是緩解那些使用C++的軟件開發(fā)者日常經(jīng)受的不知所措以及無法作出決斷的痛

苦之情。

本書致力于解決的并不是軟件開發(fā)者因經(jīng)驗不足或知識不夠而遇到的問題,而是這個職業(yè)中的所有成員,

從初學(xué)者甚至到最有才干和經(jīng)驗的那些人,所共同遭遇的問題。這些問題中部分源于語言自身固有的不完

美性,部分源于人們對語言所支持的一些概念的常見誤用。無論如何,它們給我們所有人帶來了麻煩。

本書并不僅僅是對語言中的不完美之處進(jìn)行一些簡單的論述并附帶?些“別這么做”列表,你可以找到很

多著眼于這方面的C++書籍。和它們不同,本書的重點在于如何為我所指出的缺陷(中的大部分)提供解決

方案,并借此使得這門語言變得不再像它本來那么“不完美”。木書重在賦予開發(fā)者能量,討論他們賴以

謀生的手段一C+H一中潛在的問題領(lǐng)域,給出了?些重要的相關(guān)信息,并為開發(fā)者提供了一些建議以及

經(jīng)過實踐檢驗的技巧和技術(shù),以幫助他們避免或應(yīng)付這些問題。

苦行僧式編程

在我們閱讀過的很多教科書中,即便是非常好的,也只是告訴我們C++能夠提供的解決問題的手段,前提還

得是你必須將C++中的有關(guān)特性有效地利用起來。然而,這些書常常又會在后面接著說道:“這樣做其實沒

有實質(zhì)性的意義”或者“嚴(yán)格來說那樣做就梢微有點過頭了”。不止一個以前的同事曾把我拖進(jìn)類似的激

烈爭論之中。通常人們的理由可以歸結(jié)為:“我是?個有經(jīng)驗的程序員,我并不會犯XYZ阻止我犯的那些錯

誤,干嘛要為之煩神呢?”。

唉!

這個論點簡直不堪一駁。我也是個有經(jīng)驗的程序員,但我每天至少會犯?個低級錯誤,假如不是養(yǎng)成了嚴(yán)

格的習(xí)慣的話,則會是?天十個!他們的這種態(tài)度其實就是在假定他們的代碼永遠(yuǎn)都不會被沒有經(jīng)驗的程

序員看到。此外,他們的說法要得以成立,等于是在說代碼作者永遠(yuǎn)也不會學(xué)習(xí)或者改變觀念、習(xí)慣以及

方法論。最后一點,到底怎樣才算是“有經(jīng)驗的程序員”呢?[注2]

[注2]如今這個問題似乎變得沒有定論,因為你看到每個人的履歷表上的自我評價全都是10分。

上面提到的這類人不喜歡引用、常量成員、訪問控制、explicit,具體類(concreteclasses),封裝、

不變式,他們甚至在編碼時根本就無視可移植性或可維護(hù)性。但他們就是喜歡重載、重寫(override).

隱式轉(zhuǎn)換、C風(fēng)格強(qiáng)制(C-stylecasts),并到處使用int。他們還喜歡全局變量、混合式typedef、

dynamic_cast,RTTI>專有的編譯器擴(kuò)展以及友元。他們在編碼風(fēng)格上總是不一致,讓事情看起來似乎比

原本還要復(fù)雜。

請容許我暫時扯開話題,講述一個歷史典故。自從1162年被亨利二世封為坎特伯雷大主教之后,ThomasA

Beckett就經(jīng)歷了一場人格上的轉(zhuǎn)變,從原先物質(zhì)主義的生活中改過自新,真正開始關(guān)心起貧窮的人們,并

為他以前的無度行為深刻地進(jìn)行了懺悔。后來人們在準(zhǔn)備埋葬他的軀體的時候,發(fā)現(xiàn)他穿著一件粗糙的、

爬滿跳釜的苦行衣。事后人們才知道原來他生前天天被修道士們鞭打。天哪!

我個人覺得為了懺悔和凈化自己的靈魂,這種做法未免有點過了。不過,回想起當(dāng)初自已濫用C++的強(qiáng)大能

力干了許多糟糕的事情(見附錄B),如今我々試采用?種更有節(jié)制的做法,因此就有了“在編程時穿上苦

行衣”這一說法[注3]。

[注3]如果對你們來說苦行衣的比喻有點極端,不合你的口味,你也可以和輸珈類比,即做起來辛苦,但獲

得的回報相比付出而言還是值得的。

當(dāng)然,我并不是說得像頭懸梁、錐刺股那般,也不是說我在編碼時不再聽開得震天響的舞曲,都不是,我

只是說,我會盡我所能讓我的軟件來嚴(yán)厲地對待我,以便在我企圖去誤用它時將我打住。我喜歡const,許

多const,我會在任何可能的地方使用它。我盡可能使用private,我優(yōu)先使用引用(references)。我盡

可能實施不變式。我將資源返還給它的出處,即便我知道有其他安全的“捷徑”:“呃,你知道的,它在

該操作系統(tǒng)以前的版本上可是一點問題也沒有,你更新系統(tǒng)那是你的問題,跟我可沒關(guān)系!”.我使用所

謂的“概念性typedef”來增強(qiáng)C++類型檢查。我使用九種編譯器,并借助于?個工具(見附錄C),讓它們

的使用變得更加簡便直觀。我還使用了一個更為有效的NULL

我并不是為了獲得“年度程序員”獎項提名才這么做的。這些只不過是我一直以來懶惰所導(dǎo)致的結(jié)果。所

有優(yōu)秀的工程師都仃懶惰的習(xí)慣。懶惰意味著你不想在運行期查錯:懶惰意味著你不想第二次犯同樣的錯

誤;懶惰意味著盡可能地榨取你的編譯器的能力,這樣你才得以清閑。

讓編譯器成為你的仆從

“batman”一詞起源于大英帝國時代,意指勤務(wù)兵或個人仆從。如果你能夠正確地對待編譯器的話,你就

可以讓編譯器成為你的左右手,或者說仆從(或者你的“超級英雄”,隨你喜歡)。

你的編程苦行衣越粗糙,你的編譯器就能夠為你服務(wù)得越周到。然而,有時你的編譯器對語言盡心盡責(zé)的

行為也會反過來妨礙你的意圖,頑固地拒絕履行你知道是合理的(或者至少是你想要完成的)事情。

這本書將會為你提供一些技巧和技術(shù),通過它們你得以從編譯器那兒奪回控制權(quán),并作出最終決策:獲取

你想要的而不是你被給予的。這件工作并不輕松,但我們得承認(rèn),軟件開發(fā)者才是開發(fā)過程中的主:宰,而

語言、編譯器和庫只不過是被我們所驅(qū)使的工具而已。

永不言敗

盡管我接受的大部分教育都屬于理科方面的,但實際上我更是一個工程師。我喜歡那些早期的科幻小說,

其中的那些英勇的工程師總能在面臨棘手的情況時找到出路。這也正是本書所采用的方式。理論是有的,

而且我們一開始也是跟著理論走。但每當(dāng)我們在這門語言的邊緣“行走”時,我們總會發(fā)現(xiàn)當(dāng)前大多數(shù)編

譯器都不是很遵從理論,因此我們必須在這?現(xiàn)實的約束下編程。正如YogiBerra所說的,“從理論上說,

理論與實踐是沒有任何區(qū)別的,但從實踐的角度來說,它們之間區(qū)別就大了?!?/p>

這種看待問題的方式能夠為我們帶來強(qiáng)大的效果。工程師的努力(而不是理論上的歸納),再加上頑強(qiáng)地

和C++中的不完美之處抗?fàn)?,使我最終獲得以下一系列的發(fā)現(xiàn):

?借助于墊片(Shims)(第29章)以及熱片所帶來的類型隧道(TypeTunneling)機(jī)制(第34章)進(jìn)

行顯式泛化(explicitgeneralization)。

?對C++類型系統(tǒng)進(jìn)行了些擴(kuò)展(第18章),使我們能夠?qū)⒏拍钌匣ゲ幌喔伞⒌谕愋停╞ase

type)實現(xiàn)的類型彼此區(qū)分開,并能夠基于它們進(jìn)行函數(shù)重載。

?一個編譯器無關(guān)的機(jī)制,提供了動態(tài)加載的C++對象之間的二進(jìn)制兼容性(第8章)。

?在C++規(guī)則下重現(xiàn)C中強(qiáng)大的NULL,同時不違背后者的精神(第15章)。

??個具有最大限度的“安全性”且可移植的。peratorbool()(第24章)。

?對“封裝”概念的精細(xì)分類,借此我們得以對基本的數(shù)據(jù)結(jié)構(gòu)進(jìn)行高效地表示和操縱,并擴(kuò)充我們的

“不完美工具箱”(第2、3兩章)。

?一個用于高效地分配動態(tài)大小的內(nèi)存塊的靈活工具(第32章)。

??個能夠進(jìn)行快速、非侵入式的串拼接操作的機(jī)制(第25章)。

?對“如何編寫能夠在不同的錯誤處理模型卜.工作的代碼”這一問題的中肯評價(第19章)。

?一個用于控制單件對象(singletonobjects)的順序性的宜觀機(jī)制(第11章)。

?■個在時間和空間上均高效的C++屬性(property)的實現(xiàn)(第35章)。

本書并不打算成為使用C++語言的完備指南,它只是想幫助開發(fā)者掙脫現(xiàn)實中的約束,以便找到應(yīng)對那些不

完美之處的解決方案,并鼓勵以一種超越常規(guī)的思考方式來考慮問題。

當(dāng)然,我本人也絕非完美,正所謂“金無足赤,人無完人”。我也T過蠢事,而且我還有點離經(jīng)叛道。我

有個糟糕的習(xí)慣,在應(yīng)該寫private的時候我會寫protected。在或許應(yīng)該使用lOSlream(C++輸入輸出流)

的時候我會偏好于使用printfO。我喜歡數(shù)組和指針,而且我還是C兼容API的忠實擁護(hù)者。我甚至并不是

完全忠實地遵循苦行僧式編程哲學(xué)的,但我堅信這?哲學(xué)信條是實現(xiàn)你的目標(biāo)的最可靠、最快捷的途徑,

當(dāng)然,前提是你必須隨時隨地盡可能地堅持它。

wImperfectC++”的精神

除了不完美主義實踐者的哲學(xué)信條之外,本書大體上還反映了我在編寫C++代碼時遵循的指導(dǎo)原則。它們總

的來說(盡管并非完全)跟“C精神”[Como-SOC]和“C++精神”[Como-SOP]這一對李生法則是相吻合的:

C精神:

?相信程序員:('ImperfectC++”不羞于作出丑陋的決策。

?不要阻止程序員去做必須完成的事情:"ImperfectC++”實際上會幫助你完成你想做的事情。

?保持解決方案的簡潔:書中的大部分代碼正是如此,而且是高度平臺無關(guān)和可移植的。

?使它快!即使影響到可移植性的保證也在所不惜:效率被給予高度的重視,盡管我們偶爾會為此犧牲

可移植性。

C++精神:

?C++是c的?種方言,不過C++針對現(xiàn)代軟件開發(fā)作了一些增強(qiáng):我們在不少重要的場合下仍然依賴于

和C的互操作性。

?盡管C++是一門比C龐大得多的語言,但你并不需要為你不使用的東西付出代價(這樣時間和空間的額

外開銷就會被保持為最小。而那些確實存在開銷的地方也得被整體觀察才能作出定論,因為你要比較

的是等價的程序,而不是在特性X和特性Y之間進(jìn)行比較)。

?盡可能地在編譯期捕獲錯誤:"ImperfectC++”在任何適當(dāng)?shù)牡胤绞褂渺o態(tài)斷言(staticassertions)

和約束(constraints)?

?盡量避開預(yù)處理(大多數(shù)情況下inline、const,template等才是正道):我們會看到各種各樣的技

術(shù),它們使用C++語言而不是預(yù)處理器來實現(xiàn)我們的目標(biāo)。

除了這些原則外,木書還會以身作則地示范以下的做法,即在任何可能的情況下,

?編寫不依賴于特定的編譯器(擴(kuò)展和特性)、操作系統(tǒng)、錯誤處理模型、線程模型以及字符編碼策略

的代碼;

?在不可能使用編譯期錯誤偵測的情況下采用契約式設(shè)計(Desigrrby-Contract)(見1.3節(jié))。

編碼風(fēng)格

為了使得本書的篇幅不至于過長,我不得不在示例代碼中大幅略去我慣常采用的嚴(yán)格編碼風(fēng)格(當(dāng)然,有

些人可能認(rèn)為那是“學(xué)究氣”的編碼風(fēng)格)。第17章描述了我在布局類定義代碼時通常遵循的原則。其他

編碼風(fēng)格,如大括號和間隔縮進(jìn)風(fēng)格等,就無傷大雅了。如果你有興趣,你可以很容易地從配書光盤中找

到這方面的大量材料。

術(shù)語

計算機(jī)語言就是二進(jìn)制語言,而人類的語言則是各種模糊的、不精確的語言。這里我給出一些術(shù)語的定義,

本書其余部分將沿用這些術(shù)語:

客戶代碼:表示使用其他代碼的代碼,通常(但不局限于)指的是客戶應(yīng)用程序代碼使用庫代碼的情形。

編譯單元:來自一個源文件以及該源文件所包含(依賴)的全部頭文件的全部源代碼所構(gòu)成的整體。

編譯環(huán)境:編譯環(huán)境是由編譯器、庫、操作系統(tǒng)構(gòu)成的一個整體環(huán)境,我們編寫的代碼就是在這種環(huán)境中

編譯的。感謝Kernighan和Pike[Kernl999]對此的定義。

仿函數(shù)(Functor):這是一個被廣泛使用的術(shù)語,其含義代表的是函數(shù)對象(FunctionObject)或

Functional,但這個稱呼并不是標(biāo)準(zhǔn)的稱呼。實際上我更傾向于使用函數(shù)對象(FunctionObject)這?術(shù)

語。但有人說服了我[注4],說仿函數(shù)(Functor)更好一些,因為它只有一個短短的單詞,更有特色,更

醒目,也更容易查找,特別是在網(wǎng)上查找的時候。

[注4]要責(zé)怪就責(zé)怪本書的幾個審閱者好了。

泛用性(Generality):我或許從來都沒有弄懂“泛型(genericity)”這個詞,或者說至少沒有弄懂它

在編程上下文中的含義,盡管我有時候也會把這個詞拿出來用一下。我猜它的意思是“編寫對一系列類型

都能夠起作用的模板代碼,這些類型在它們被使用的方式(而不是定義)上有聯(lián)系”,只有在這種情況下

我才會使用“泛型(genericity)w這個詞。而Generality(泛用性)[Kernl999]則似乎表意更恰當(dāng)一些,

感覺上更適用于表達(dá)這一概念,因為我不僅關(guān)心我的代碼是否能夠在其他(模板參數(shù))類型上工作,同樣

也關(guān)心它們能否跟其他頭文件和庫起工作。

除了這些概念上的術(shù)語之外,我還使用了一些特殊的語言相關(guān)的術(shù)語。我不知道你怎么想,但我確實發(fā)現(xiàn)

C++中的術(shù)語挺混亂的,因此我打算花一點時間來回顧一些定義。下面的定義來白C++標(biāo)準(zhǔn),但我將它們以

一種更簡單的方式呈現(xiàn)出來,以便于我們理解,而不僅僅是我個人的理解。這些定義中有些是重疊的,因

為雖說它們針對的是不同的概念,但都是C++實踐者的字典里的一部分。這里的“C++實踐者”也包括我所

說的不完美主義實踐者。

基本類型和復(fù)合類型

基本類型(C++-98:3.9.1)包括整型(char、short、int、long(longlong/—int64),以上這些類型

的有符號和無符號的版本[注5],以及bool)、浮點型(float,double以及l(fā)ongdouble)以及void類型。

[注5]注意,char有三種"型號":char、unsignedchar以及signedchar。我們會在第13章看到,這也會

帶來一些問題。

復(fù)合類型(C++-98:3.9.2)基本上就是表示除基本類型之外的其他所有類型,包括:數(shù)組、函數(shù)、指針(任

何類型的指針,包括指向非靜態(tài)成員的指針)、引用、類、聯(lián)合(unions)以及枚舉。

我傾向于不使用“復(fù)合類型”這個術(shù)語,因為我覺得它的名字好像是在說它是由其他東西所“組成”的?

樣,但實際上對于指針和引用來說根本就不是這么一回事。

對象類型

對象類型(C++-98:3.9;9)包括任何“不是函數(shù)類型、不是引用類型、也不是void類型”的類型。我同

樣不喜歡使用這個術(shù)語,因為按照這種說法它的范疇并不僅僅包括“類類型(classtypes)的實例”,可

是實際上人們卻往往會這么想。所以我在全書中凡涉及到這種地方一律用“實例”這一術(shù)語。

標(biāo)量類型和類類型

標(biāo)量類型(Scalartypes)(C++-98:3.9;10)包括“算術(shù)類型、枚舉類型以及指針類型"。類類型(C++-98:

9)即是指以class、struct或union這三個關(guān)鍵字之一所聲明的東西。

結(jié)構(gòu)是一個由struct關(guān)鍵字進(jìn)行定義的類類型,其成員和基類的訪問控制缺省情況卜均為public。聯(lián)合是

一個通過union關(guān)鍵字定義的類類型,其成員缺省情況下也是public的。類則是由class關(guān)鍵字定義的類類

型,其成員和基類的訪問控制在缺省情況下均為private。

聚合體

標(biāo)準(zhǔn)(C++-98:8.5.1;1)這樣來描述聚合體(aggregate):“一個數(shù)組,或一個無用戶自定義構(gòu)造函數(shù)、

無private或protected的非靜態(tài)數(shù)據(jù)成員、無基類并且無虛函數(shù)的類”。作為一個數(shù)組或一個類類型,它

意味著將多個東西聚合進(jìn)一個東西中,故有“聚合體”一說。

聚合體的初始化方式可以是?對包含有初始化子句的大括號,像這樣:

structX

(

inti;

shortas[2];

}x={10,{20,30}};

盡管聚合體通常由POD類型(見下?節(jié))組成,但未必非得如此。在上面的代碼中,X的成員變量i也可以是

類類型的,只要它具有一個接受單個整型參數(shù)的非顯式構(gòu)造函數(shù)(non-explicitconstructor)(見2.2.7

節(jié))和一個可用的拷貝構(gòu)造函數(shù)即可。

POD類型

POD意思是“plain-old-data”(C++-98:1.8;5),它是C++中的一個非常重要的概念,但通常會被人們

誤解,而且雖說它是個“小東西”,但卻相當(dāng)重要。其定義也相當(dāng)糟糕。標(biāo)準(zhǔn)給出了兩個線索。(C++-98:

3.9;2)說:“任何完備的(complete)[譯注1]的POD類型T……必須滿足以下條件:將組成它的一個對象

的各字節(jié)拷貝到一個字節(jié)數(shù)組中,然后再將它們重新拷貝回原先的對象所占的存儲區(qū)中,此時該對象應(yīng)該

仍具有它原來的值?!比欢?C++-98:3.9;3)處,我們又被告知:“對于任何POD類型T,如果有兩個

指針分別指向不同的T對象objl和obj2,此時如果使用memcpy()將objl的值拷貝進(jìn)obj2中,obj2就應(yīng)該跟

objl具有相同的值?!?/p>

[譯注1]完備的(complete)與非完備的(incomplete)相對。一個類類型如果只有聲明沒有定義的話,就

稱它是非完備的,反之則是完備的。一般而言,一個類型要成為完備的,前提是其內(nèi)存布局必須是已知的。

唔,這些說法相當(dāng)華而不實,不是嗎?值得慶幸的是,為了充實這個定義,C++標(biāo)準(zhǔn)在它776頁篇幅的內(nèi)容

中另有若干處(準(zhǔn)確地說,是56處)零星地提到了關(guān)于POD類型的一些信息。

在[Como-POD]中,GregComeau指出,大多數(shù)C++書籍根本不提POD,并且說“大多數(shù)C++書籍都不值得購買”。

帶著為了提高銷量這個“憤世嫉俗”的企圖,我打算盡量把POD描述得到位一些。這應(yīng)該不會太難,因為Greg

在他的文章中已經(jīng)提供了POD結(jié)構(gòu)的所有本質(zhì)特征,那我就全當(dāng)搭順風(fēng)車?yán)?。Let'sGO!

標(biāo)準(zhǔn)(C+—98:3.9;10)對POD類型的總體性定義如卜一:“標(biāo)量類型、POD結(jié)構(gòu)類型、POD聯(lián)合類型,以上

這些類型的數(shù)組,以及這些類型以const/volatile修飾的版本?!鄙厦孢@個定義,除了POD結(jié)構(gòu)和POD聯(lián)合

有待澄清外,其余都I卜:常清楚。

POD結(jié)構(gòu)(C++-98:9;4)是“一個聚合體類,其任何非靜態(tài)成員的類型都不能是如下任一種:指向成員的

指針、'非POD'結(jié)構(gòu)、'非POD'聯(lián)合,以及以上這些類型的數(shù)組或引用,同時該聚合體類不允許包含用

戶自定義的拷貝賦值操作符和用戶自定義的析構(gòu)函數(shù)?!盤OD聯(lián)合的定義類似,只不過它是個聯(lián)合而不是

結(jié)構(gòu)而已。注意,聚合體類不僅可以是結(jié)構(gòu)(struct),也可以是類(class)。

到目前為止一切都沒問題,但POD類型到底有什么重要之處呢?呃,POD類型允許我們與C函數(shù)進(jìn)行交互,它

們是C與C++之間互通的手段,因此推而廣之就成了C++與外部世界溝通的橋梁(見7、8、9三章)。因此,

POD結(jié)構(gòu)或POD聯(lián)合的存在就允許我們“知道?個普通的結(jié)構(gòu)或聯(lián)合在C里面是什么樣子的”[Como-POD]。

POD最好的助記辦法就是將它看成是一種與C兼容的類型,當(dāng)然同時你也不能忘記有關(guān)POD的方方面面。

POD其他重要的方面包括:

?宏offsetofO所作用于的類型應(yīng)該是“一個POD結(jié)構(gòu)或一個POD聯(lián)合"(C++-98:18.1;5),此外用

在任何其他類型(見2.3.2節(jié)和第35章)身上都是未定義的。

?POD類型可以被放在聯(lián)合中。這個特性被我用來制造個約束(見1.2.4節(jié)),該約束的作用是約束?

個類型為POD類型(見1.2.4節(jié))。

?如果?個POD類型的靜態(tài)變量是以常量表達(dá)式來初始化的話,那么其初始化發(fā)生于執(zhí)行流進(jìn)入它所在

的語句塊之前,而且也是在任何(不管是POD類型還是其他類型)需要動態(tài)初始化的對象的初始化之

前(見第11章)。

?指向成員的指針不是POD類型,這一點跟其他任何指針類型恰恰相反。

?POD結(jié)構(gòu)或POD聯(lián)合類型可以具有靜態(tài)成員、成員typedef、嵌套類型和方法[注6]。

[注6]當(dāng)然,這些東西對于任何C代碼來說都應(yīng)該是不可見的,因此,如果你的代碼必須是C/C++兼容的話,

你就應(yīng)當(dāng)使用條件編譯將它們排除在外,只有當(dāng)處于C++編譯環(huán)境中時才讓它們成為可見的。

第1章強(qiáng)制設(shè)計:約束、契約和斷言

在我們設(shè)計軟件時,我們希望軟件根據(jù)設(shè)計而進(jìn)行使用。這并非一句空話。在大多數(shù)情況卜,很容易發(fā)生

以意料之外的方式來使用軟件,而這么做的結(jié)果往往是令人失望的。

大多數(shù)軟件的文檔幾乎都是不完整,甚至是過時的,我堅信你也有這方面的經(jīng)驗。這并非單純的錯誤或缺

失,“如果還有比沒有文檔更糟的情形,那就是文檔是錯誤的”[Meyel997]。如果被使用的組件比較簡單,

使用得當(dāng),或者說是標(biāo)準(zhǔn)的或被普遍使用的,那么沒有文檔倒也不是什么大問題。例如,如果許多程序員

需要一而再、再而三地查找C庫函數(shù)mallocO的用法,那可真算是奇聞怪談了。然而,這種情況畢竟很少。

我曾經(jīng)遇到?些程序員,他們非常有經(jīng)驗,但對malloc。的兄弟reallocO和free。之間的細(xì)微差別卻并

不那么熟悉。

對于這些問題,解決的方式很多。其是通過增強(qiáng)的參數(shù)驗證,使軟件組件具有更強(qiáng)的抵抗錯誤的能力,

但這種方式通常不那么有吸引力,因為它會損及性能,還傾向于滋生壞習(xí)慣。制作良好的文檔并讓它們保

持更新當(dāng)然是解決方案的一個重要組成部分,然而這種做法是遠(yuǎn)遠(yuǎn)不夠的,因為它是“非強(qiáng)制性”的。此

外,要想寫出好文檔也是一件極其困難的事情[Hunt2000]。軟件越復(fù)雜,其原始作者要想把自己擺在對該

軟件懵懂無知的處境從而便于寫出更好的說明文檔就越不可能,而獨立的技術(shù)作者要想抓住其所有的細(xì)微

之處就更加困難了。因此,當(dāng)情況不再單純時,?個確保正確使用代碼的更好的方式顯然是必不可少的。

如果編譯器能夠為我們找出錯誤那就更可取了。事實上,本書中的相當(dāng)一部分內(nèi)容都是關(guān)于如何驅(qū)使和利

用編譯器,令它在碰到糟糕的代碼時卡殼,從而便于我們在編譯期及早抓住錯誤。但愿你能夠意識到花兒

分鐘來安撫編譯器比花上幾個小時和調(diào)試器糾纏要好得多。正如Kernighan和Pike在《ThePracticeof

Programming》[Kenml999]中所說的那樣,“無論你喜歡與否,調(diào)試是一門我們經(jīng)常要實踐的藝術(shù)……如果

不產(chǎn)生bug就好了,所以我們嘗試在第一時間就把代碼寫正確,從而盡量避免bug的產(chǎn)生由于我并不比

其他軟件工程師更勤快,因此我總是盡量讓編譯器幫我做事情。從長遠(yuǎn)來看,“苦行僧”式的編程是比較容

易的選擇。然而,并非所有的錯誤都能夠在編譯期查出來。在這種情況下,我們需要求助于運行期機(jī)制。

一些語言(例如D和Eiffel)提供了內(nèi)建的機(jī)制,即通過“契約式設(shè)計(DesignbyContract),(來確保

軟件按照其設(shè)計而被使用。此項技術(shù)的先驅(qū)是BertrandMeyer[Meyel997],它源于程序的形式驗證。契約

式設(shè)計要求為組件指定“契約”,這些契約會在程序運行過程中的某些特定點被強(qiáng)制執(zhí)行。契約在很多方面

都可以作為文檔的替代品,因為它們無法被忽略,并且是自動進(jìn)行驗證的。此外,通過遵循特定的語法約

定,它就可以和自動化文檔工具合作,我們將會在1.3節(jié)對此進(jìn)行討論。

實施“強(qiáng)制<enforcement)"的機(jī)制之一是斷言(assertion),包括廣為人知的運行期斷言,以及較少為

人知然而甚至更為有用的編譯期斷言。本書中兩者均得到了大量的使用,因此我們將會在L4節(jié)對這種重

要的工具進(jìn)行詳細(xì)的觀察。

1.1雞蛋和火腿

我并不懷疑我很可能是在班門弄斧,然而有些事情很重要,在此不得不說。因此,請各位允許我嘮叨片刻:

?在設(shè)計期捕獲bug比在編碼/編譯期捕獲好。[注1]

?在編碼/編譯期捕獲bug比在單元測試中捕獲好、[注2]

?在單元測試中捕獲bug比在調(diào)試中捕獲好。

?在調(diào)試中捕獲bug比在預(yù)發(fā)行/beta版中捕獲好。

?在預(yù)發(fā)行/beta版中捕獲bug比讓你的客戶捕獲好。

?讓你的客戶捕獲bug(以具有親和力的方式)比沒有客戶好。

[注1]我并非瀑布模型的擁護(hù)者,所以編碼期和編譯期對于我來說都是?樣。不過,縱然我喜歡單元測試,

并且體驗過一些快速結(jié)對編程(pair-programming)合作,我仍然不認(rèn)為我是一個XP(極限編程)[Beck2000]

熱衷者。

[注2]這假定你做了單元測試。如果你沒有,那么你需要開始這么做一現(xiàn)在就開始!

這些都是相當(dāng)明顯的東西,盡管客戶可能并不贊同最后一條。最好把那條留給我們自己。

實施強(qiáng)制有兩種方式:在編譯期和在運行期。這些正是本章要講述的內(nèi)容。

1.2編譯期契約:約束

本章講述編譯期強(qiáng)制,通常它也被稱為“約束(constraints)”。遺憾的是,C++并不直接支持約束。

Imperfection:C++并不直接支持約束。

C++是?門極其強(qiáng)大和靈活的語言,因此很多支持者(甚至包括些C++權(quán)威)都會認(rèn)為本節(jié)描述的約束實

現(xiàn)技術(shù)已經(jīng)足夠了。然而,作為C++和約束的雙重?fù)碜o(hù)者,我必須提出我的異議(由于一些很平常的原因)。

雖然我并不買其他語言鼓吹者的賬,然而我同樣認(rèn)為閱讀因違反約束而導(dǎo)致的編譯錯誤信息是很困難(甚

至極其困難)的。它們通常令人眼花繚亂,有時甚至深奧無比。如果你是“肇事”代碼的作者,那么問題

通常并不嚴(yán)重,然而,當(dāng)面對的是一個多層模板實例化的情形時,其失敗所導(dǎo)致的錯誤信息近乎天書,難

以理解,甚至非常優(yōu)秀的編譯器都是如此。木節(jié)后面我們會看到一些約束,以及違反它們所導(dǎo)致的錯誤信

息,還有可以令錯誤信息稍微更具可讀性的一些措施。

1.2.1must_have_base()

這個例子是從comp.lang.c++.moderated新聞組上幾乎原樣照搬過來的,是BjarneStroustrup發(fā)的帖子,

他稱之為“Has_base"。[Sutt2002]對此亦有描述,不過換了個名字,叫做IsDerivedFrom,而我則喜歡將

約束的名字以“must,開頭,因此我把它命名為must_have_base。

程序清單L1

template<typenameD

,typenameB

>

structmust_have_base

(

'wust_have_base()

(

void(*p)(D*,B*)=constraints;

)

private:

staticvoidconstraints(D*pd,B*pb)

(

pb=pd;

}

};

它的工作原理如下:在成員函數(shù)constraints()中之試把I)類型的指針賦值給B類型的指針(1)和B都是模

板參數(shù)),constraints。是一?個單獨的靜態(tài)成員函數(shù),這是為了確保它永遠(yuǎn)不會被調(diào)用,因此這種方案沒

有任何運行期負(fù)擔(dān)。析構(gòu)函數(shù)中則聲明了一個指向constraints。的指針,從而強(qiáng)迫編譯器至少要去評估

該函數(shù)以及其中的賦值語句是否有效。

事實上,這個約束的名字有點不恰當(dāng):如果D和B是同樣的類型,那么這個約束仍然能夠被滿足,因此,

它或許應(yīng)該更名為must_have_base_or_be_same_type,或者類似這樣的名字。另一個替代方案是把

must_have_base進(jìn)一步精化,令它不接受D和B是同一類型的情況。請將你的答案寫在明信片上寄給我。

另外,如果D和B的繼承關(guān)系并非公有繼承,那么該約束也會失敗。在我看來,這是個命名方式的問題,

而不是約束本身的能力缺陷問題,因為我只需要對采用公有繼承的類型應(yīng)用這個約束。[注3]

[注3]這聽起來像是循環(huán)論證,不過我敢保證它不是。

由于"must_have_base”在其定義中所進(jìn)行的動作恰好體現(xiàn)了約束本身的語義,所以如果該約束失敗,錯

誤信息會相當(dāng)直觀。事實上,我手頭所有編譯器(見附錄A)對此均提供了非常有意義的信息,即要么提

到兩個類型不具有繼承關(guān)系,要么提到兩個指針類型不能互相轉(zhuǎn)換,或與此類似的信息。

1.2.2must_be_subscriptable()

另一個有用的約束是要求一個類型可以按卜.標(biāo)方式訪問(見14.2節(jié)),實現(xiàn)起來很簡單:

template<typenameT>

structmust_be_subscriptable

(

staticvoidconstraints(Tconst&T_is_not_subscriptable)

(

sizeof(T_is_not_subscriptable[0]);

)

為了提高可讀性,constraints。的形參被命名為T_is_notsubscriptable,這可以為那些違反約束的可

憐的人兒提供些許線索??紤]卜.面的例子:

structsubs

(

public:

intoperator[](size_tindex)const;

)

structnotsubs

{);

must_be_subscriptable<int[]>a;//ini*可以按下標(biāo)方式訪問

must_be_subscriptable<int*>b;//ini*可以按下標(biāo)方式訪問

must_be_subscriptable<subs>c;//subs可以按下標(biāo)方式訪問

must_be_subscriptable<not_subs>d;//notsubs不可以按下標(biāo)方式訪問,編譯錯誤

Borland5.6給出了令人難以置信的信息:operator+'notimplementedintype'〈type〉'forarguments

oftype'int'infunctionmust_be_subscriptab1e<not_subs>::constraints(constnot_subs而

當(dāng)模板實例化深度多達(dá)15層時,不絞盡腦汁簡直是不可能搞清楚這些錯誤信息的含義的。

相較而言,DigitalMars更為準(zhǔn)確一些,不過仍然不夠好:aError:arrayorpointerrequiredbefore

'[';Had:constnot_subs,\

另外一些編譯器的錯誤信息都包含了變量名"T_is_not_subscriptable”。其中最優(yōu)秀的當(dāng)數(shù)VisualC++

給出的錯誤信息:"binary':'conststructnotsubs'doesnotdefinethisoperatororaconversion

toatypeacceptabletothepredefinedoperatorwhi1ecompilingclass-templatememberfunction

*voidmust_be_subscriptable<structnot_subs>::constraints(conststructnot_subs&)”。

1.2.3must_be_subscriptab1e_as_decayab1e_pointer()

在第14章中,我們會深入考察數(shù)組和指針之間的關(guān)系,并了解有關(guān)指針偏移的?些詭秘的特性,它致使

^offset[pointer]n這種語法成為跟"poinler[offsel]”一樣合法且等價的形式(這也許使Borland編

譯器關(guān)于mustbe_subscriptable的莫名其妙的編譯錯誤看起來不是那么毫無意義,不過它對于我們追蹤

并理解約束是如何被違反的確實有很大的幫助。)由于這種顛倒形式對于重教了“operator口”的類

(class)類型是無效的,因此must_be_subscriptable還可以被進(jìn)一步精化,從而把它作用的類型約束到

僅限于(原生)指針類型。

程序清單L2

template<typenameT>

structmustbesubscriptable_asdecayablepointer

staticvoidconstraints(Tconst&Tis_notdecay_subscriptable)

sizeof(0[T_is_not_decay_subscriptable]);譯注:⑴I欄,弋,

//僅僅作用于原生指針。

);

很明顯,所有可以通過offset[pointer]形式進(jìn)行索引訪問的pointer也都可以接受pointer[offset]操作,

因此不需要把must_be_subscriptable合并到must_be_subscriptab1e_as_decayab1e_pointer中去。不過,

盡管這兩種約束有著不同的結(jié)果,但利用繼承機(jī)制將二者結(jié)合起來也是合適之舉。

現(xiàn)在我們可以區(qū)分(原生)指針和其他可進(jìn)行索引訪問的類型了:

must_be_subscriptable<subs>a;//ok

must_be_subscriptable_as_decayable_pointer<subs>b;//編譯錯誤

1.2.4must_be_pod()

你會在木書中多次看到mustbepod()的使用(見19.5、19.7、21.2.1和32.2.3節(jié))。這是我編寫的第一

個約束,那時我根本不知道約束是什么,甚至連POD是什么意思都不清楚(見“序言”)。must_be_pod()

非常簡單。

C++98標(biāo)準(zhǔn)9.5節(jié)第1條說明:“如果一個類具有非平凡的(non-trivial)構(gòu)造函數(shù),非平凡的拷貝構(gòu)造

函數(shù),非平凡的析構(gòu)函數(shù),或者非平凡的賦值操作符,那么其對象不能作為聯(lián)合(union)的成員。”這恰

好滿足我們的需要,并且,我們還可以設(shè)想,這個約束和我們已經(jīng)看到的那些模樣差不多:有個

constraints。成員函數(shù),其中包含一個union:

template<typenameT>

structmust_be_pod

(

staticvoidconstraints()

(

union

(

TT_is_not_POD_type;

);

)

遺憾的是,這是編譯器容易發(fā)生奇怪行為的領(lǐng)域,所以真正的定義沒有這么簡單,而是需要許多預(yù)處理器

操作(見1.2.6節(jié)),但效果還是一樣的。

在19.7節(jié)中,我們將會看到這個約束和■個更專門化的約束must_be_pod_or_void()?起使用,目的在于

能夠檢查某個指針?biāo)傅氖欠駷榉瞧椒驳念愵愋?。為此,我們需要對must_be_pod_or_void模板進(jìn)行特化

[Vand2003],而其泛化的定義則與mustbepod相同:

template<typenameT>

structmust_be_pod_or_void

...//和must_be_pod一樣

);

template<>

structmust_be_pod_or_void<void>

//該特化的定義是空的,里面什么也沒有,所以不會找編譯器的麻煩。

};

同樣,編譯器對于違反must_be_pod/must_be_pod_or_void約束所生成的信息也是各式各樣的:

classNonPOD

public:

virtual~NonP0D();

};

must_be_pod<int>a;//int是POD(見"序言")

must_be_pod<not_subs>b;//notsubs是POD(見"序言")

must_bepod<NonPOD>c;//NonPOD不是POD,編譯錯誤

這一次,DigitalMars一慣的簡練風(fēng)格卻給我們帶來了麻煩,因為我們能得到的錯誤信息只是“Error:union

memberscannothavectorsordtors,\指向約束類內(nèi)部引發(fā)編譯錯誤的那行代碼。如果這是在一個大項

目里的話,那么很難追溯到錯誤的源頭,即最初引發(fā)這個錯誤的實例化點。而Walcom對于這么一個極小的

錯誤給出的信息則是最多的:“Error!E183:col(10)unionscannothavememberswithconstructors;

Note!N633:col(10)templateclassinstantiationformust_be_pod<NonF,OD>,was

in:..\constraints_test.cpp(106)(col48)”。

1.2.5must_be_same_size()

最后一個約束mustbe_same.size。也在本書的后續(xù)部分被使用到(見21.2.1節(jié)和25.5.5節(jié))。該約束類

使用靜態(tài)斷言“無效數(shù)組大小”技術(shù)來確保兩個類型的大小是一致的,我們很快就會在1.4.7節(jié)看到該技

術(shù)。

程序清單1.3

template<typenameT1

,typenameT2

>

structmust_be_same_size

(

private:

staticvoidconstraints0

(

constintT1_not_same_size_as_T2

=sizeof(Tl)==sizeof(T2);

inti[Tl_not_same_size_as_T2];

)

);

如果兩個類型大小不一致,那么TlnotsamesizeasT2就會被求值為(編譯期)常數(shù)0,而將0作為數(shù)

組大小是非法的。

至于mustbepodorvoid,則是在兩個類型中有一個或都是void時會用到它。然而,由于sizeof(void)

是非法的表達(dá)式,因此我們必須提供一些額外的編譯期功能。

如果兩個類型都是void,則很容易,我們可以這樣來特化must_be_same_size類模板:

template<>

structmust_be_same_size<void,void>

{);

然而,如果只有?個類型是void,要使其工作可就沒那么直截了當(dāng)了。解決方案之一是利用局部特化機(jī)制

[Vand2003],然而并非所有目前廣為使用的編譯器都支持這種能力。進(jìn)一步而言,我們還得同時為這個模

板準(zhǔn)備一個完全特化和兩個局部特化版本(兩個局部特化分別將第一個和第二個模板參數(shù)特化為void)。

最后,我們還得構(gòu)思某種方式來提供哪怕是“有點兒”意義的編譯期借誤信息。我沒有借助于這種方法,

而是通過令void成為“可size_of的”來解決這個問題。我的方案實現(xiàn)起來極其容易,并且不需要局部特

化:

程序清單1.4

template<typenameT>

structsize_of

(

enum{value=sizeof(T)};

);

template<>

structsize_of<void>

(

enum{value=0);

);

現(xiàn)在我們所要做的就是在must_be_same_size中用size_of來替代sizeof:

template<...>

structmust_be_samesize

(

staticvoidconstraints0

(

constintT1mustbesamesizeasT2

二size_of<Tl>:rvalue二=size_of<T2>:rvalue;

inti[Tl_must_be_same_size_as_T2];

);

現(xiàn)在我們可以對任何類型進(jìn)行驗證了

must_be_same_size<int,int>a;//ok

nnjst_besamesize<int,long>b;//依賴于硬件架構(gòu)或編譯器

must_be_same_size<void,void>c;//ok

must_be_same_size<void,int>d;//編譯錯誤:void的“大小”是0!

正如前面的約束所表現(xiàn)出來的,不同的編譯器提供給程序員的信息量有相當(dāng)大的差別。Borland和Digital

Mars在這方面又?jǐn)∠玛噥?,它們提供的上下文信息極少甚至沒有。在這方面,我認(rèn)為Intel做得最好,它

提到"zeroTengthbitfieldmustbeunnamed指出出錯的行,并且提供了兩個直接調(diào)用上下文,其

中包括T1的實際類型和T2的實際類型,加在一起一共四行編譯器輸出。

1.2.6使用約束

我比較喜歡通過宏來使用我的約束,宏的名字遵循constraint_<constraint_name>的形式[注4],例

如constraint_must_have_base()0這從多方面來說都是非常有意義的。

[注4]正如12.4.4節(jié)所描述的原因,把宏命名為大寫形式總是好習(xí)慣。之所以我對約束的宏沒有采用同樣

的命名習(xí)慣,是因為我想要和約束類型保持一致(小寫)。事后我才發(fā)現(xiàn)這么做令它們看起來不怎么顯眼。

你自己編寫的約束當(dāng)然可以采用大寫風(fēng)格。

首先,容易無歧義地查找到它們。正因為如此,我才為約束保留了“must」前綴。也許有人會爭論說這個

需求已經(jīng)被滿足了。然而使用宏的做法更具有“自描述”性。對觀者而言,在代碼中看到

"conslraint_must_be_pod()”,其意義更加明確。

其次,使用宏的形式更具有(語法上的)?致性。盡管我沒有寫過任何非模板的約束,然而并沒有任何理

由限制其他人那么做。此外,我發(fā)現(xiàn)尖括號除了導(dǎo)致眼睛疲勞外,沒有什么其他好處。

再次,如果約束被定義在某個名字空間中,那么在使用它們時必須加上冗長的名字限定,而宏則可以輕易

地將名字限定隱藏起來,免得用戶去使用淘氣的using指令(見34.2.2節(jié))。

最后一個原因更實際。不同的編譯器對相同的約束的處理具有細(xì)微的差別,為此需要對它們耍弄一些手段。

例如,針對不同的編譯器,constraint_must_be_pod()被定義成如下三種形式之一:

do{must_be_pod<T>::func_ptr_typeconstpfn二

must_bepod<T>:constraint();}while(0)

或者

do{inti=sizeof(must_be_pod<T>:constraint0);}while(0)

或者

STATIC_ASSERT(sizeof(must_be_pod<T>::constraint())!=0)

利用宏,客戶代碼變得更加簡潔優(yōu)雅,否則它們會遭受大量的面目丑陋的代碼的干擾。

1.2.7約束和TMP

本書的一位審稿人提到部分約束可以通過TMP(templatemeta-programming,模板元編程)[注5]實現(xiàn),

他說的很對。例如,must_be_pointer約束就可以通過結(jié)合運用靜態(tài)斷言(見1.4.7節(jié))和is_pointer_type

(見33.3.2節(jié))來實現(xiàn)。如下:

#defineconstraint_must_be_pointer(T)\

STATIC_ASSERT(O!=is_pointer_type<T>::value)

[注5]我故意讓這本書盡量少地涉及模板元編程技術(shù),因為它本身就是一個相當(dāng)大的主題,而且和我所討

論到的那些uimperfectionsw并沒有直接的關(guān)系。你可以在Boost、RangeLib>STLSoft庫(包含在配書

光盤中)或其他坐書[Vand2003,Alex20()l]中找到許多模板元編程例子。

我之所以不采用TMP有幾方面原因。首先,約束的編碼看上去總是那么宜觀,因為一個約束只不過是對被

約束類型所應(yīng)該具備的行為的“模擬”。而對于TMPtraits就不能這么說了,有些TMPtraits可能會相當(dāng)

復(fù)雜。因此,約束較之模板元編程更易于閱讀。

其次,在許多(盡管并非全部)情況下,要想“說服”編譯器給出容易理解的信息,約束比traits(和靜

態(tài)斷言)更容易一些。

最后,有些約束無法通過TMPtrail來實現(xiàn),或者至少只能在很少一部分編譯器上實現(xiàn)。甚至可以說,約

束越是簡單,用TMPtraits來實現(xiàn)它就越困難,對此must_be_pod就是一個極好的例子。

HerbSutter在[Sutl2002]中曾示范了結(jié)合運用約束和traits的技術(shù),當(dāng)你進(jìn)行自己的實際工作中時;沒

有任何理由阻止你那么做。對我來說,我只不過更喜歡保持它們簡潔且獨立而已。

1.2.8約束:尾聲

本章所講述的約束當(dāng)然并非全部,然而,它們可以告訴你哪些東西是可實現(xiàn)的。約束的不足之處和靜態(tài)斷

言一樣(見1.4.8節(jié)),那就是產(chǎn)生的錯誤信息不是特別容易理解。根據(jù)實現(xiàn)約束的機(jī)制的不同,你可能會

看到明白如"typecannotbeconverted之類的錯誤信息,也可能會看到令人極其困惑的"Destructorfor

'Tisnotaccessibleinfunction<non-existent-function>"之類的信息。

只要有可能,你應(yīng)該通過為你的約束選擇合適的變量或常量名字來改善錯誤信息。我們在本節(jié)已經(jīng)看到了

這方面的例子:T_is_not_subscriptable、T_is_not_POD_type以及Tl_not_same_size_as_T2。記住,要

確保你選擇的名字反映了出錯的情況。想想違反了你的約束卻看到諸如

*'Tis_valid_type_for_constraint”之類的信息的可憐人兒吧!

最后,關(guān)于約束,還有一個值得強(qiáng)調(diào)的重要方面:隨著我們對編譯期計算和模板元編程的認(rèn)識越來越清晰,

我們可能會想去更新某些約束。也許你已經(jīng)從本書中描述的某些組件中看出我并非模板元編程方面的大師,

不過重點是,通過良好地設(shè)計用于表現(xiàn)和使用約束的方式,我們可以在以后學(xué)到更多的技巧時再來“無縫”

地更新我們的約束。我承認(rèn)我自己有好多次就是這么做的,我不覺得這是什么丟人的事情,不過,我早期

在編寫約束方面的嘗試如果拿出來現(xiàn)眼倒的確令人赧顏(我沒有在附錄B中提到它們,因為它們還沒有差

勁到那個地步,遠(yuǎn)遠(yuǎn)沒有?。?。

1.3運行期契約:前條件、后條件和不變式

“如果例程的所有前條件(precondition)已經(jīng)被調(diào)用者滿足了,那么該例程必須確保當(dāng)它完成時所有后

條件(postconditions)(以及任何不變式)皆為真。”

----HuntandThomas,ThePragmaticProgrammers[Hunt2000].

如果我們無法執(zhí)行編譯期強(qiáng)制,那么還可以采用運行期強(qiáng)制。運行期強(qiáng)制的一個系統(tǒng)化的實現(xiàn)途徑是指定

函數(shù)契約。函數(shù)契約精確定義了在函數(shù)被調(diào)用之前調(diào)用者必須滿足哪些條件(前條件),以及在函數(shù)返回之

時哪些條件(后條件)是調(diào)用者可以期望的。契約的定義以及它們的強(qiáng)制實施是DbC(DesignbyContract,

契約式設(shè)計)[Meyel997]的基石。

前條件是指函數(shù)履行其契約所必須滿足的條件。滿足前條件是調(diào)用者的責(zé)任,而被調(diào)用者則假定它的前條

件已經(jīng)被滿足,并且僅當(dāng)它的前條件被滿足時才負(fù)責(zé)提供正確的行為。這一點非常重要,在[Meye1997]

中被強(qiáng)調(diào)指出。倘若調(diào)用者沒有滿足前條件,則被調(diào)用者做出任何事情都是完全合理的。事實上,通常這

會引發(fā)一個斷言(見1.4節(jié)),進(jìn)而可能導(dǎo)致程序終止。這聽起來似乎頗令人恐慌,剛接觸DbC的程序員通

常會對此感到很不舒服,直到你問起他們“如果個函數(shù)的(前提)條件都不能被滿足,那還能指望它有

什么樣的行為呢?”才啞口無言。事實上,契約越嚴(yán)格,違反它所導(dǎo)致的后果越嚴(yán)重,從而軟件的質(zhì)量就

會越好、當(dāng)轉(zhuǎn)到DbC上時,要理解這一點是最為困難的。

后條件在函數(shù)執(zhí)行完畢時必須為真。確保后條件被滿足是被調(diào)用者的責(zé)任。當(dāng)函數(shù)返回控制時調(diào)用者可以

假定后條件已經(jīng)得到了滿足。然而,在現(xiàn)實中,有些時候有所保留(不要把賭注全部押在被調(diào)用者身上)

還是必要的,例如,當(dāng)調(diào)用應(yīng)用服務(wù)器中的第三方插件時就是如此。然而,我認(rèn)為前面所講的原則仍然是

對的。事實上,對違反契約的插件的合理反應(yīng)之一是將它卸載掉,并給公司經(jīng)理以及第三方插件廠商發(fā)一

封電子郵件。既然我們對于違反契約的行為可以作出任何反應(yīng),那么有什么理由不這么做呢?

前條件和后條件可以被應(yīng)用到類的成員函數(shù),也可以被用到自由函數(shù)身上,這對于C++(更一般地說,面

向?qū)ο缶幊蹋﹣碚f很有益處。事實上,還有另外一個與DbC相關(guān)的東西,它只能依附于類而存在,那就是

類不變式(classinvariant)?類不變式是指一個或一組條件式,它們對于一個處于良好定義狀態(tài)的對象

總是為真。根據(jù)定義,類的構(gòu)造函數(shù)負(fù)責(zé)確保類的實例進(jìn)入?個符合該類的不變式的狀態(tài)中,而類的

(public)成員函數(shù)則在它們完成之際確保類的實例仍然處在該狀態(tài)中。僅當(dāng)處于構(gòu)造函數(shù)、析構(gòu)函數(shù)或

其他某個成員函數(shù)的執(zhí)行過程中時,類不變式才不一定要為真。

在某些場合下,將不變式的作用范圍定義為比”單個對象的狀態(tài)”的范圍更廣可能更合適一些。原則上,

不變式可以被應(yīng)用到操作環(huán)境的整個狀態(tài)上,然而,在實踐中,這種情況是極其少見的,類不變式則很常

見。因此,在本章以及本書剩余的篇幅中,如果提到不變式,均是指類不變式。

對部分或根本沒有進(jìn)行封裝的類型提供不變式是可行的(見3.2節(jié)和4.4.1節(jié)),這個不變式是由與該類型

相關(guān)的API函數(shù)(以及該函數(shù)的前條件)來強(qiáng)制實施的。事實上,當(dāng)使用這種類型時:不變式是極好的主

意,因為它們?nèi)狈Ψ庋b性的特質(zhì)提高了濫用的風(fēng)險。不過這種不變式相當(dāng)容易被“繞過”,這也說明了為什

么通常應(yīng)該避免使用這種類型。事實上,[Stro2003]中某種程度上提到:如果存在?個不變式,則公有數(shù)

據(jù)簡直毫無意義。封裝既是關(guān)于隱藏實現(xiàn)又是關(guān)于保護(hù)不變式的。至于“屬

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論