C++箴言:必須返回對象時(shí)別返回引用 一旦程序員抓住對象傳值的效率隱憂,很多人就會(huì)成為狂熱的圣戰(zhàn)分子,誓要根除傳值的罪惡,無論它隱藏多深。他們不屈不撓地追求傳引用的純度,但他們?nèi)挤噶艘粋€(gè)致命的錯(cuò)誤:他們開始傳遞并不存在的對象的引用。這可不是什么好事。
考慮一個(gè)代表有理數(shù)的類,包含一個(gè)將兩個(gè)有理數(shù)相乘的函數(shù):
operator* 的這個(gè)版本以傳值方式返回它的結(jié)果,而且如果你沒有擔(dān)心那個(gè)對象的構(gòu)造和析構(gòu)的代價(jià),你就是在推卸你的專業(yè)職責(zé)。如果你不是迫不得已,你不應(yīng)該為這樣的一個(gè)對象付出成本。所以問題就在這里:你是迫不得已嗎? 哦,如果你能用返回一個(gè)引用來作為代替,你就不是迫不得已。但是,請記住一個(gè)引用僅僅是一個(gè)名字,一個(gè)實(shí)際存在的對象的名字。無論何時(shí)只要你看到一個(gè)引用的聲明,你應(yīng)該立刻問自己它是什么東西的另一個(gè)名字,因?yàn)樗囟ㄊ悄澄锏牧硪粋€(gè)名字。在這個(gè) operator* 的情況下,如果函數(shù)返回一個(gè)引用,它必須返回某個(gè)已存在的而且其中包含兩個(gè)對象相乘的產(chǎn)物的 Rational 對象的引用。 當(dāng)然沒有什么理由期望這樣一個(gè)對象在調(diào)用 operator* 之前就存在。也就是說,如果你有
似乎沒有理由期望那里碰巧已經(jīng)存在一個(gè)值為十分之三的有理數(shù)。不是這樣的,如果 operator* 返回這樣一個(gè)數(shù)的引用,它必須自己創(chuàng)建那個(gè)數(shù)字對象。 一個(gè)函數(shù)創(chuàng)建一個(gè)新對象僅有兩種方法:在棧上或者在堆上。棧上的生成物通過定義一個(gè)局部變量而生成。使用這個(gè)策略,你可以用這種方法試寫 operator*:
你可以立即否決這種方法,因?yàn)槟愕哪繕?biāo)是避免調(diào)用構(gòu)造函數(shù),而 result 正像任何其它對象一樣必須被構(gòu)造。一個(gè)更嚴(yán)重的問題是這個(gè)函數(shù)返回一個(gè)引向 result 的引用,但是 result 是一個(gè)局部對象,而局部對象在函數(shù)退出時(shí)被銷毀。那么,這個(gè) operator* 的版本不會(huì)返回引向一個(gè) Rational 的引用——它返回引向一個(gè)前 Rational;一個(gè)曾經(jīng)的 Rational;一個(gè)空洞的、惡臭的、腐敗的,從前是一個(gè) Rational 但永不再是的尸體的引用,因?yàn)樗呀?jīng)被銷毀了。任何調(diào)用者甚至于沒有來得及匆匆看一眼這個(gè)函數(shù)的返回值就立刻進(jìn)入了未定義行為的領(lǐng)地。這是事實(shí),任何返回一個(gè)引向局部變量的引用的函數(shù)都是錯(cuò)誤的。(對于任何返回一個(gè)指向局部變量的指針的函數(shù)同樣成立。) 那么,讓我們考慮一下在堆上構(gòu)造一個(gè)對象并返回引向它的引用的可能性。基于堆的對象通過使用 new 而開始存在,所以你可以像這樣寫一個(gè)基于堆的 operator*:
哦,你還是必須要付出一個(gè)構(gòu)造函數(shù)調(diào)用的成本,因?yàn)橥ㄟ^ new 分配的內(nèi)存要通過調(diào)用一個(gè)適當(dāng)?shù)臉?gòu)造函數(shù)進(jìn)行初始化,但是現(xiàn)在你有另一個(gè)問題:誰是刪除你用 new 做出來的對象的合適人選? 即使調(diào)用者盡職盡責(zé)且一心向善,它們也不太可能是用這樣的方案來合理地預(yù)防泄漏:
這里,在同一個(gè)語句中有兩個(gè) operator* 的調(diào)用,因此 new 被使用了兩次,這兩次都需要使用 delete 來銷毀。但是 operator* 的客戶沒有合理的辦法進(jìn)行那些調(diào)用,因?yàn)樗麄儧]有合理的辦法取得隱藏在通過調(diào)用 operator* 返回的引用后面的指針。這是一個(gè)早已注定的資源泄漏。 但是也許你注意到無論是在棧上的還是在堆上的方法,為了從 operator* 返回的每一個(gè) result,我們都不得不容忍一次構(gòu)造函數(shù)的調(diào)用。也許你想起我們最初的目標(biāo)是避免這樣的構(gòu)造函數(shù)調(diào)用。也許你認(rèn)為你知道一種方法能避免除一次以外幾乎全部的構(gòu)造函數(shù)調(diào)用。也許下面這個(gè)實(shí)現(xiàn)是你做過的,一個(gè)基于 operator* 返回一個(gè)引向 static Rational 對象的引用的實(shí)現(xiàn),而這個(gè) static Rational 對象定義在函數(shù)內(nèi)部:
就像所有使用了 static 對象的設(shè)計(jì)一樣,這個(gè)也會(huì)立即引起我們的線程安全(thread-safety)的混亂,但那是它的比較明顯的缺點(diǎn)。為了看到它的更深層的缺陷,考慮這個(gè)完全合理的客戶代碼:
猜猜會(huì)怎么樣?不管 a,b,c,d 的值是什么,表達(dá)式 ((a*b) == (c*d)) 總是等于 true! 如果代碼重寫為功能完全等價(jià)的另一種形式,這一啟示就很容易被理解了:
注意,當(dāng) operator== 被調(diào)用時(shí),將同時(shí)存在兩個(gè)起作用的對 operator* 的調(diào)用,每一個(gè)都將返回引向 operator* 內(nèi)部的 static Rational 對象的引用。因此,operator== 將被要求比較 operator* 內(nèi)部的 static Rational 對象的值和 operator* 內(nèi)部的 static Rational 對象的值。如果它們不是永遠(yuǎn)相等,那才真的會(huì)令人大驚失色了。 這些應(yīng)該足夠讓你信服試圖從類似 operator* 這樣的函數(shù)中返回一個(gè)引用純粹是浪費(fèi)時(shí)間,但是你們中的某些人可能會(huì)這樣想“好吧,就算一個(gè) static 不夠用,也許一個(gè) static 的數(shù)組是一個(gè)竅門……” 我無法拿出示例代碼來肯定這個(gè)設(shè)計(jì),但我可以概要說明為什么這個(gè)想法應(yīng)該讓你羞愧得無地自容。首先,你必須選擇一個(gè) n 作為數(shù)組的大小。如果 n 太小,你可能會(huì)用完存儲(chǔ)函數(shù)返回值的空間,與剛剛名譽(yù)掃地的 single-static 設(shè)計(jì)相比,在任何一個(gè)方面你都不會(huì)得到更多的東西。但是如果 n 太大,就會(huì)降低你的程序的性能,因?yàn)樵诤瘮?shù)第一次被調(diào)用的時(shí)候數(shù)組中的每一個(gè)對象都會(huì)被構(gòu)造。即使這個(gè)我們正在討論的函數(shù)僅被調(diào)用了一次,也將讓你付出 n 個(gè)構(gòu)造函數(shù)和 n 個(gè)析構(gòu)函數(shù)的成本。如果“優(yōu)化”是提高軟件效率的過程,對于這種東西也只能是“悲觀主義”的。最后,考慮你怎樣將你所需要的值放入數(shù)組的對象中,以及你做這些需要付出什么。在兩個(gè)對象間移動(dòng)值的最直接方法就是通過賦值,但是一次賦值將要付出什么?對于很多類型,這就大約相當(dāng)于調(diào)用一次析構(gòu)函數(shù)(銷毀原來的值)加上調(diào)用一次構(gòu)造函數(shù)(把新值拷貝過去)。但是你的目標(biāo)是避免付出構(gòu)造和析構(gòu)成本!面對的結(jié)果就是:這個(gè)方法絕對不會(huì)成功。(不,用一個(gè) vector 代替數(shù)組也不會(huì)讓事情有多少改進(jìn)。) 寫一個(gè)必須返回一個(gè)新對象的函數(shù)的正確方法就是讓那個(gè)函數(shù)返回一個(gè)新對象。對于 Rational 的 operator*,這就意味著下面這些代碼或在本質(zhì)上與其相當(dāng)?shù)哪承〇|西:
當(dāng)然,你可能付出了構(gòu)造和析構(gòu) operator* 的返回值的成本,但是從長遠(yuǎn)看,這只是為正確行為付出的很小的代價(jià)。除此之外,這種令你感到恐怖的賬單也許永遠(yuǎn)都不會(huì)到達(dá)。就像所有的程序設(shè)計(jì)語言,C++ 允許編譯器的實(shí)現(xiàn)者在不改變生成代碼的可觀察行為的條件下使用優(yōu)化來提升它的性能,在某些條件下會(huì)產(chǎn)生如下結(jié)果:operator* 的返回值的構(gòu)造和析構(gòu)能被安全地消除。如果編譯器利用了這一點(diǎn)(編譯器經(jīng)常這樣做),你的程序還是在它假定的方法上繼續(xù)運(yùn)行,只是比你期待的要快。 全部的焦點(diǎn)在這里:如果需要在返回一個(gè)引用和返回一個(gè)對象之間做出決定,你的工作就是讓那個(gè)選擇能提供正確的行為。讓你的編譯器廠商去絞盡腦汁使那個(gè)選擇盡可能地廉價(jià)。 ·絕不要返回一個(gè)局部棧對象的指針或引用,絕不要返回一個(gè)被分配的堆對象的引用,如果存在需要一個(gè)以上這樣的對象的可能性時(shí),絕不要返回一個(gè)局部 static 對象的指針或引用。 |
|
|