# Item 21: 當你必須返回一個對象時不要試圖返回一個引用
作者:Scott Meyers
譯者:fatalerror99 (iTePub's Nirvana)
發布:http://blog.csdn.net/fatalerror99/
一旦程序員抓住對象傳值的效率隱憂(參見 Item 20),很多人就會成為狂熱的圣戰分子,誓要根除傳值的罪惡,無論它隱藏多深。他們不屈不撓地追求傳引用的純度,但他們全都犯了一個致命的錯誤:他們開始傳遞并不存在的對象的引用。這可不是什么好事。
考慮一個代表有理數的類,包含一個將兩個有理數相乘的函數:
```
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\* 的這個版本以傳值方式返回它的結果,而且如果你沒有擔心那個對象的構造和析構的代價,你就是在推卸你的專業職責。如果你不是迫不得已,你不應該為這樣的一個對象付出成本。所以問題就在這里:你是迫不得已嗎?
哦,如果你能用返回一個引用來作為代替,你就不是迫不得已。但是,請記住一個引用僅僅是一個名字,一個實際存在的對象的名字。無論何時只要你看到一個引用的聲明,你應該立刻問自己它是什么東西的另一個名字,因為它必定是某物的另一個名字。在這個 operator\* 的情況下,如果函數返回一個引用,它必須返回某個已存在的而且其中包含兩個對象相乘的產物的 Rational 對象的引用。
當然沒有什么理由期望這樣一個對象在調用 operator\* 之前就存在。也就是說,如果你有
```
Rational a(1, 2); // a = 1/2
Rational b(3, 5); // b = 3/5
Rational c = a * b; // c should be 3/10
```
似乎沒有理由期望那里碰巧已經存在一個值為十分之三的有理數。不是這樣的,如果 operator\* 返回這樣一個數的引用,它必須自己創建那個數字對象。
一個函數創建一個新對象僅有兩種方法:在棧上或者在堆上。棧上的生成物通過定義一個局部變量而生成。使用這個策略,你可以用這種方法試寫 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;
}
```
你可以立即否決這種方法,因為你的目標是避免調用構造函數,而 result 正像任何其它對象一樣必須被構造。一個更嚴重的問題是這個函數返回一個引向 result 的引用,但是 result 是一個局部對象,而局部對象在函數退出時被銷毀。那么,這個 operator\* 的版本不會返回引向一個 Rational 的引用——它返回引向一個前 Rational;一個曾經的 Rational;一個空洞的、惡臭的、腐敗的,從前是一個 Rational 但永不再是的尸體的引用,因為它已經被銷毀了。任何調用者甚至于沒有來得及匆匆看一眼這個函數的返回值就立刻進入了未定義行為的領地。這是事實,任何返回一個引向局部變量的引用的函數都是錯誤的。(對于任何返回一個指向局部變量的指針的函數同樣成立。)
那么,讓我們考慮一下在堆上構造一個對象并返回引向它的引用的可能性。基于堆的對象通過使用 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;
}
```
哦,你還是必須要付出一個構造函數調用的成本,因為通過 new 分配的內存要通過調用一個適當的構造函數進行初始化,但是現在你有另一個問題:誰是刪除你用 new 做出來的對象的合適人選?
即使調用者盡職盡責且一心向善,它們也不太可能是用這樣的方案來合理地預防泄漏:
```
Rational w, x, y, z;
w = x * y * z; // same as operator*(operator*(x, y), z)
```
這里,在同一個語句中有兩個 operator\* 的調用,因此 new 被使用了兩次,這兩次都需要使用 delete 來銷毀。但是 operator\* 的客戶沒有合理的辦法進行那些調用,因為他們沒有合理的辦法取得隱藏在通過調用 operator\* 返回的引用后面的指針。這是一個早已注定的資源泄漏。
但是也許你注意到無論是在棧上的還是在堆上的方法,為了從 operator\* 返回的每一個 result,我們都不得不容忍一次構造函數的調用。也許你想起我們最初的目標是避免這樣的構造函數調用。也許你認為你知道一種方法能避免除一次以外幾乎全部的構造函數調用。也許下面這個實現是你做過的,一個基于 operator\* 返回一個引向 static Rational 對象的引用的實現,而這個 static Rational 對象定義在函數內部:
```
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 對象的設計一樣,這個也會立即引起我們的線程安全(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 的值是什么,表達式 ((a*b) == (c*d)) 總是等于 true!
如果代碼重寫為功能完全等價的另一種形式,這一啟示就很容易被理解了:
```
if (operator==(operator*(a, b), operator*(c, d)))
```
注意,當 operator== 被調用時,將同時存在兩個起作用的對 operator\* 的調用,每一個都將返回引向 operator\* 內部的 static Rational 對象的引用。因此,operator== 將被要求比較 operator\* 內部的 static Rational 對象的值和 operator\* 內部的 static Rational 對象的值。如果它們不是永遠相等,那才真的會令人大驚失色了。
這些應該足夠讓你信服試圖從類似 operator\* 這樣的函數中返回一個引用純粹是浪費時間,但是你們中的某些人可能會這樣想“好吧,就算一個 static 不夠用,也許一個 static 的數組是一個竅門……”
我無法拿出示例代碼來肯定這個設計,但我可以概要說明為什么這個想法應該讓你羞愧得無地自容。首先,你必須選擇一個 n 作為數組的大小。如果 n 太小,你可能會用完存儲函數返回值的空間,與剛剛名譽掃地的 single-static 設計相比,在任何一個方面你都不會得到更多的東西。但是如果 n 太大,就會降低你的程序的性能,因為在函數第一次被調用的時候數組中的每一個對象都會被構造。即使這個我們正在討論的函數僅被調用了一次,也將讓你付出 n 個構造函數和 n 個析構函數的成本。如果“優化”是提高軟件效率的過程,對于這種東西也只能是“悲觀主義”的。最后,考慮你怎樣將你所需要的值放入數組的對象中,以及你做這些需要付出什么。在兩個對象間移動值的最直接方法就是通過賦值,但是一次賦值將要付出什么?對于很多類型,這就大約相當于調用一次析構函數(銷毀原來的值)加上調用一次構造函數(把新值拷貝過去)。但是你的目標是避免付出構造和析構成本!面對的結果就是:這個方法絕對不會成功。(不,用一個 vector 代替數組也不會讓事情有多少改進。)
寫一個必須返回一個新對象的函數的正確方法就是讓那個函數返回一個新對象。對于 Rational 的 operator\*,這就意味著下面這些代碼或在本質上與其相當的某些東西:
```
inline const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
```
當然,你可能付出了構造和析構 operator\* 的返回值的成本,但是從長遠看,這只是為正確行為付出的很小的代價。除此之外,這種令你感到恐怖的賬單也許永遠都不會到達。就像所有的程序設計語言,C++ 允許編譯器的實現者在不改變生成代碼的可觀察行為的條件下使用優化來提升它的性能,在某些條件下會產生如下結果:operator\* 的返回值的構造和析構能被安全地消除。如果編譯器利用了這一點(編譯器經常這樣做),你的程序還是在它假定的方法上繼續運行,只是比你期待的要快。
全部的焦點在這里:如果需要在返回一個引用和返回一個對象之間做出決定,你的工作就是讓那個選擇能提供正確的行為。讓你的編譯器廠商去絞盡腦汁使那個選擇盡可能地廉價。
Things to Remember
* 絕不要返回一個局部棧對象的指針或引用,絕不要返回一個被分配的堆對象的引用,如果存在需要一個以上這樣的對象的可能性時,絕不要返回一個局部 static 對象的指針或引用。(Item 4 提供的一個返回一個局部 static 的設計的例子是合理的,至少在單線程的環境中是這樣。)
- Preface(前言)
- Introduction(導言)
- Terminology(術語)
- Item 1: 將 C++ 視為 federation of languages(語言聯合體)
- Item 2: 用 consts, enums 和 inlines 取代 #defines
- Item 3: 只要可能就用 const
- Item 4: 確保 objects(對象)在使用前被初始化
- Item 5: 了解 C++ 為你偷偷地加上和調用了什么函數
- Item 6: 如果你不想使用 compiler-generated functions(編譯器生成函數),就明確拒絕
- Item 7: 在 polymorphic base classes(多態基類)中將 destructors(析構函數)聲明為 virtual(虛擬)
- Item 8: 防止因為 exceptions(異常)而離開 destructors(析構函數)
- Item 9: 絕不要在 construction(構造)或 destruction(析構)期間調用 virtual functions(虛擬函數)
- Item 10: 讓 assignment operators(賦值運算符)返回一個 reference to *this(引向 *this 的引用)
- Item 11: 在 operator= 中處理 assignment to self(自賦值)
- Item 12: 拷貝一個對象的所有組成部分
- Item 13: 使用對象管理資源
- Item 14: 謹慎考慮資源管理類的拷貝行為
- Item 15: 在資源管理類中準備訪問裸資源(raw resources)
- Item 16: 使用相同形式的 new 和 delete
- Item 17: 在一個獨立的語句中將 new 出來的對象存入智能指針
- Item 18: 使接口易于正確使用,而難以錯誤使用
- Item 19: 視類設計為類型設計
- Item 20: 用 pass-by-reference-to-const(傳引用給 const)取代 pass-by-value(傳值)
- Item 21: 當你必須返回一個對象時不要試圖返回一個引用
- Item 22: 將數據成員聲明為 private
- Item 23: 用非成員非友元函數取代成員函數
- Item 24: 當類型轉換應該用于所有參數時,聲明為非成員函數
- Item 25: 考慮支持不拋異常的 swap
- Item 26: 只要有可能就推遲變量定義
- Item 27: 將強制轉型減到最少
- Item 28: 避免返回對象內部構件的“句柄”
- Item 29: 爭取異常安全(exception-safe)的代碼
- Item 30: 理解 inline 化的介入和排除
- Item 31: 最小化文件之間的編譯依賴
- Item 32: 確保 public inheritance 模擬 "is-a"
- Item 33: 避免覆蓋(hiding)“通過繼承得到的名字”
- Item 34: 區分 inheritance of interface(接口繼承)和 inheritance of implementation(實現繼承)
- Item 35: 考慮可選的 virtual functions(虛擬函數)的替代方法
- Item 36: 絕不要重定義一個 inherited non-virtual function(通過繼承得到的非虛擬函數)
- Item 37: 絕不要重定義一個函數的 inherited default parameter value(通過繼承得到的缺省參數值)
- Item 38: 通過 composition(復合)模擬 "has-a"(有一個)或 "is-implemented-in-terms-of"(是根據……實現的)
- Item 39: 謹慎使用 private inheritance(私有繼承)
- Item 40: 謹慎使用 multiple inheritance(多繼承)
- Item 41: 理解 implicit interfaces(隱式接口)和 compile-time polymorphism(編譯期多態)
- Item 42: 理解 typename 的兩個含義
- Item 43: 了解如何訪問 templatized base classes(模板化基類)中的名字
- Item 44: 從 templates(模板)中分離出 parameter-independent(參數無關)的代碼
- Item 45: 用 member function templates(成員函數模板) 接受 "all compatible types"(“所有兼容類型”)
- Item 46: 需要 type conversions(類型轉換)時在 templates(模板)內定義 non-member functions(非成員函數)
- Item 47: 為類型信息使用 traits classes(特征類)
- Item 48: 感受 template metaprogramming(模板元編程)
- Item 49: 了解 new-handler 的行為
- Item 50: 領會何時替換 new 和 delete 才有意義
- Item 51: 編寫 new 和 delete 時要遵守慣例
- Item 52: 如果編寫了 placement new,就要編寫 placement delete
- 附錄 A. 超越 Effective C++
- 附錄 B. 第二和第三版之間的 Item 映射