# Item 28: 避免返回對象內部構件的“句柄”
作者:Scott Meyers
譯者:fatalerror99 (iTePub's Nirvana)
發布:http://blog.csdn.net/fatalerror99/
假設你正在一個包含矩形的應用程序上工作。每一個矩形都可以用它的左上角和右下角表示出來。為了將一個 Rectangle 對象保持在較小狀態,你可能決定那些點的定義的域不應該包含在 Rectangle 本身之中,更合適的做法是放在一個由 Rectangle 指向的輔助的結構體中:
```
class Point { // class for representing points
public:
Point(int x, int y);
...
void setX(int newVal);
void setY(int newVal);
...
};
struct RectData { // Point data for a Rectangle
Point ulhc; // ulhc = " upper left-hand corner"
Point lrhc; // lrhc = " lower right-hand corner"
};
class Rectangle {
...
private:
std::tr1::shared_ptr<RectData> pData; // see Item 13 for info on
}; // tr1::shared_ptr
```
由于 Rectangle 的客戶需要有能力操控 Rectangle 的區域,因此類提供了 upperLeft 和 lowerRight 函數。可是,Point 是一個用戶定義類型,所以,留心 Item 20 關于在典型情況下,以傳引用的方式傳遞用戶定義類型比傳值的方式更加高效的觀點,這些函數返回引向底層 Point 對象的引用:
```
class Rectangle {
public:
...
Point& upperLeft() const { return pData->ulhc; }
Point& lowerRight() const { return pData->lrhc; }
...
};
```
這個設計可以編譯,但它是錯誤的。實際上,它是自相矛盾的。一方面,upperLeft 和 lowerRight 是被聲明為 const 的成員函數,因為它們被設計成僅僅給客戶提供一個獲得 Rectangle 的點的方法,而不允許客戶改變這個 Rectangle(參見 Item 3)。另一方面,兩個函數都返回引向私有的內部數據的引用——調用者可以利用這些引用修改內部數據!例如:
```
Point coord1(0, 0);
Point coord2(100, 100);
const Rectangle rec(coord1, coord2); // rec is a const rectangle from
// (0, 0) to (100, 100)
rec.upperLeft().setX(50); // now rec goes from
// (50, 0) to (100, 100)!
```
請注意這里,upperLeft 的調用者是怎樣利用返回的 rec 的內部 Point 數據成員的引用來改變這個成員的。但是 rec 卻被期望為 const!
這直接引出兩條經驗。第一,一個數據成員被封裝,但是具有最高可訪問級別的函數還是能夠返回引向它的引用。在當前情況下,雖然 ulhc 和 lrhc 被聲明為 private,它們還是被有效地公開了,因為 public 函數 upperLeft 和 lowerRight 返回了引向它們的引用。第二,如果一個 const 成員函數返回一個引用,引向一個與某個對象有關并存儲在這個對象本身之外的數據,這個函數的調用者就可以改變那個數據(這正是二進制位常量性的局限性(參見 Item 3)的一個副作用)。
我們前面做的每件事都涉及到成員函數返回的引用,但是,如果它們返回指針或者迭代器,因為同樣的原因也會存在同樣的問題。引用,指針,和迭代器都是句柄(handle)(持有其它對象的方法),而返回一個對象內部構件的句柄總是面臨危及對象封裝安全的風險。就像我們看到的,它同時還能導致 const 成員函數改變了一個對象的狀態。
我們通常認為一個對象的“內部構件”就是它的數據成員,但是不能被常規地公開訪問的成員函數(也就是說,它是 protected 或 private 的)也是對象內部構件的一部分。同樣地,不要返回它們的句柄也很重要。這就意味著你絕不應該有一個成員函數返回一個指向擁有較小的可訪問級別的成員函數的指針。如果你這樣做了,它的可訪問級別就會與那個擁有較大的可訪問級別的函數相同,因為客戶能夠得到指向這個擁有較小的可訪問級別的函數的指針,然后就可以通過這個指針調用這個函數。
無論如何,返回指向成員函數的指針的函數是難得一見的,所以讓我們把注意力返回到 Rectangle 類和它的 upperLeft 和 lowerRight 成員函數。我們在這些函數中挑出來的問題都只需簡單地將 const 用于它們的返回類型就可以排除:
```
class Rectangle {
public:
...
const Point& upperLeft() const { return pData->ulhc; }
const Point& lowerRight() const { return pData->lrhc; }
...
};
```
通過這個修改的設計,客戶可以讀取定義一個矩形的 Points,但他們不能寫它們。這就意味著將 upperLeft 和 upperRight 聲明為 const 不再是一句空話,因為他們不再允許調用者改變對象的狀態。至于封裝的問題,我們總是故意讓客戶看到做成一個 Rectangle 的 Points,所以這是封裝的一個故意的放松之處。更重要的,它是一個有限的放松:只有讀訪問是被這些函數允許的,寫訪問依然被禁止。
雖然如此,upperLeft 和 lowerRight 仍然返回一個對象內部構件的句柄,而這有可能造成其它方面的問題。特別是,這會導致空懸句柄:引用了不再存在的對象的構件的句柄。這種消失的對象的最普通的來源就是函數返回值。例如,考慮一個函數,返回在一個矩形窗體中的 GUI 對象的 bounding box:
```
class GUIObject { ... };
const Rectangle // returns a rectangle by
boundingBox(const GUIObject& obj); // value; see Item 3 for why
// return type is const
```
現在,考慮客戶可能會這樣使用這個函數:
```
GUIObject *pgo; // make pgo point to
... // some GUIObject
const Point *pUpperLeft = // get a ptr to the upper
&(boundingBox(*pgo).upperLeft()); // left point of its
// bounding box
```
對 boundingBox 的調用會返回一個新建的臨時的 Rectangle 對象。這個對象沒有名字,所以我們就稱它為 temp。于是 upperLeft 就在 temp 上被調用,這個調用返回一個引向 temp 的一個內部構件的引用,特別是,它是由 Points 構成的。隨后 pUpperLeft 指向這個 Point 對象。到此為止,一切正常,但是我們無法繼續了,因為在這個語句的末尾,boundingBox 的返回值—— temp ——被銷毀了,這將間接導致 temp 的 Points 的析構。接下來,剩下 pUpperLeft 指向一個已經不再存在的對象;pUpperLeft 空懸在創建它的語句的末尾!
這就是為什么任何返回一個對象的內部構件的句柄的函數都是危險的。它與那個句柄是指針,引用,還是迭代器沒什么關系。它與是否受到 cosnt 的限制沒什么關系。它與那個成員函數返回的句柄本身是否是 const 沒什么關系。全部的問題在于一個句柄被返回了,因為一旦這樣做了,你就面臨著這個句柄比它引用的對象更長壽的風險。
這并不意味著你永遠不應該讓一個成員函數返回一個句柄。有時你必須如此。例如,operator[] 允許你從 string 和 vector 中取出單獨的元素,而這些 operator[]s 就是通過返回引向容器中的數據的引用來工作的(參見 Itme 3)——當容器本身被銷毀,數據也將銷毀。盡管如此,這樣的函數屬于特例,而不是慣例。
Things to Remember
* 避免返回對象內部構件的句柄(引用,指針,或迭代器)。這樣會提高封裝性,幫助 const 成員函數產生 cosnt 效果,并將空懸句柄產生的可能性降到最低
- 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 映射