【轉】http://www.shnenglu.com/tiandejian/archive/2007/09/23/ec_28.html
第28條: 不要返回指向對象內部部件的“句柄”
假設你正在設計一個與矩形相關的應用程序。每個矩形的區域都由它的左上角和右下角的坐標來表示。為了 讓 Rectangle 對象盡可能的小巧,你可能會做出這樣的決定: Rectangle 自身并不保存這些點的坐標的信息,取而代之的是將這些信息保存在一個輔助結構中,然后讓 Rectangle 指向它:
class Point { // 表示點的類
public:
Point(int x, int y);
...
void setX(int newVal);
void setY(int newVal);
...
};
struct RectData { // 供 Rectangle 類使用的點的數據
Point ulhc; // ulhc = " 左上角點的坐標
Point lrhc; // lrhc = " 右下角點的坐標 "
};
class Rectangle {
...
private:
std::tr1::shared_ptr<RectData> pData;
// 關于 tr1::shared_ptr 請參見第 13 條
};
因為 Rectangle 的客戶端程序員可能需要了解矩形的區域,所以這個類就應該提供 upperLeft 和 lowerRight 函數。然而, Point 卻是一個用戶自定義的類型,因此你可能會回憶起第 20 條的經驗:通過引用傳遞用戶自定義類型的對象要比直接傳值更高效,這些函數可以返回引用來指向更底層的 Point 對象:
class Rectangle {
public:
...
Point& upperLeft() const { return pData->ulhc; }
Point& lowerRight() const { return pData->lrhc; }
...
};
這樣的設計可以通過編譯,但是它卻是錯誤的。實際上,它是自我矛盾的。另外,由于 upperLeft 和 lowerRight 的設計初衷僅僅是為客戶端程序員提供一個途徑來了解 Rectangle 的兩個頂點坐標在哪里,而不是讓客戶端程序員去修改它,因此這兩個函數應聲明為 const 成員函數。另外,這兩個函數都返回指向私有內部數據的引用——通過這些引用,調用者可以任意修改內部數據!請看下邊的示例:
Point coord1(0, 0);
Point coord2(100, 100);
const Rectangle rec(coord1, coord2);// rec 是一個 Rectangle 常量
// 兩頂點是 (0, 0), (100, 100)
rec.upperLeft().setX(50); // 但現在 rec 的兩頂點卻變為
// (50, 0), (100, 100)!
upperLeft 返回了 rec 內部的 Point 數據成員,在這里請注意:雖然 rec 本身應該是 const 的,但是調用者竟可以使用 upperLeft 所返回的引用來修改這個數據成員!
上面的現象立刻引出了兩個議題:首先,數據成員僅僅與訪問限制最為寬泛的函數擁有同等的封裝性。在這種情況下,即使 ulhc 和 lrhc 聲明為私有的,它們實際上仍然是公共的,這是因為公共函數 upperLeft 和 lowerRight 返回了指向它們的引用。其次,如果一個 const 成員函數返回一個引用,這一引用指向的數據與一個對象相關,但這一數據卻保存在該對象以外,那么函數的調用者就可以修改這一數據。(這樣恰巧超出了按位恒定的范疇——參見第 3 條。)
我們所做的一切都與返回引用的成員函數有關,但是如果它們返回的是指針或者迭代器,同樣的問題仍然會因為同樣的理由發生。引用、指針、迭代器都可以稱作“句柄”(獲取其它對象的渠道),返回一個指向對象內部部件的句柄,通常都會危及到對象的封裝性。就像我們看到的,即使成員函數是 const 的,返回對象的狀態也是可以任意更改的。
大體上講,對象的“內部部件”主要是它的數據成員,但是非公用的成員函數同樣也是對象的內部部件。與數據成員相同,返回指向成員函數的句柄也是糟糕的設計。這意味著你不應該讓一個公用成員函數 A 返回一個指向非公用成員函數 B 的指針。如果你這樣做了, B 的訪問權層次就與 A 一樣了,這是因為客戶端程序員將能夠取得 B 的指針,然后通過這一指針來調用它。
索性的是,返回指向成員函數指針的函數并不常見,所以讓我們還是把精力放在 Rectangle 類和他的 upperLeft 和 lowerRight 成員函數上來吧。我們所發現的關于這些函數所存在的兩個問題都可以簡單的解決,只要將它們的返回值限定為 const 的就可以了:
class Rectangle {
public:
...
const Point& upperLeft() const { return pData->ulhc; }
const Point& lowerRight() const { return pData->lrhc; }
...
};
使用這一改進的設計方案,客戶端程序員就可以讀取用來定義一個矩形的兩個點,但是他們不可以修改這兩個點。這就意味著將 upperLeft 和 lowerRight 聲明為 const 的并不是一個假象,因為它們將不允許調用者來修改對象的狀態。至于封裝問題,我們一直堅持讓客戶端程序員能能夠看到構造一個 Rectangle 的兩個 Point ,所以說這里我們故意放松了封裝的限制。更重要的是,這一放松是有限的:這些函數僅僅提供了讀的訪問權限。寫權限仍然是禁止的。
class GUIObject { ... };
const Rectangle boundingBox(const GUIObject& obj);
// 以傳值方式返回一個矩形。關于返回值為什么是 const 的,請參見第 3 條
現在請考慮一下客戶端程序員可能怎樣來使用這個函數:
GUIObject *pgo; // 讓 pgo 指向某個 GUIObject
const Point *pUpperLeft = &(boundingBox(*pgo).upperLeft());
// 取得一個指向 boundingBox 左上角點的指針
調用 boundingBox 將會返回一個新的、臨時的 Retangle 對象。這個對象沒有名字,所以姑且叫它 temp 。隨后 temp 將調用 upperLeft ,然后此次調用將返回一個指向 temp 內部部件的引用,特別地,指向構造 temp 的一個點。 pUpperleft 將會指向這一 Point 對象。到目前為止一切都很完美,但是任務尚未完成,因為在這一語句的最后, boundingBox 的返回值—— temp ——將會被銷毀,這樣間接上會導致 temp 的 Point 被銷毀掉。于是, pUpperLeft 將會指向一個并不存在的對象。這條語句創建了 pUpperLeft ,可也讓它成了孤魂野鬼。
為什么說:任何返回指向對象內部部件句柄的函數都是危險的,這個問題已經一目了然了。至于句柄是指針還是引用還是迭代器,函數是否是 const 的,成員函數返回的句柄本身是不是 const 的,這一切都無關緊要。只有一點,那就是:只要返回了一個句柄,那么就意味著你正在承擔風險:它可能會比它指向的對象存活更長的時間。
這并不意味著你永遠也不能讓一個成員函數返回一個句柄。有些時候你不得不這樣做。比如說, operator[] 允許你獲取 string 和 vector 中的任一元素,這些 operator[] 的工作就是通過返回容器內部的數據來完成的(參見第 3 條)——當容器本身被銷毀時,這些數據同時也會被銷毀。然而,這僅僅是一個例外,不是慣例。
銘記在心
l 避免返回指向對象內部部件的句柄(引用、指針或迭代器)。這樣做可以增強封裝性,幫助 const 成員函數擁有更加“ const ”的行為,并且使“野句柄”出現的幾率降至最低。