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