Item 28: Avoid returning handles to object internals.
不要返回對象私有成員的句柄。這裡的“句柄”(handle)包括引用、指針和迭代器。 這樣可以增加類的封裝性、使得const
函數更加const
, 也避免了空引用的創建(dangling handles)。
為了方便起見,下文中統一用指針來稱呼這三類句柄。
在繼續Scott Meyers的討論之前,先來回顧一下類成員指針的行為。 首先如果不加限制,直接返回私有成員的指針會導致私有成員被完全暴露。例如:
class Rectangle {
int _left, _top;
public:
int& left() { return _left; }
};
Rectangle rec;
rec.left() = 3; // rec._left被修改
其實這已經足以說明返回私有成員指針相當於完全暴露了私有成員。
如果是為了保護私有成員不被修改,只是為了讓外界可以不通過函數就可以訪問_left
, 可以將left()
聲明為const
:
int& left() const{ return _left; }
上述代碼還有問題~ 編譯器會產生如下錯誤:
error: binding of reference to type 'int' to a value of type 'const int'
drops qualifiers
這是因為常量方法不能修改當前對象,其返回值也應該是const
的。應該這樣寫:
const int& left() const{ return _left; }
由於返回值聲明了const
,客戶修改內部變量便會編譯錯了。我們成功地在開放了內部變量的同時防止了內部變量被修改。但是問題沒有到此為止!還記得嗎?C++的常量定義為bitwise constness,只要當前對象沒被修改就算常量。所以如果我們將left
和top
存在類的外面,常量方法的返回值類型檢查便會失效,比如把數據存在Point
裡面:
class Point{
public:
int left, right;
};
class Rectangle {
Point* p;
public:
int& left() const { return p->left; }
};
...
const Rectangle rec;
rec.left() = 3; // rec明明是const對象,但我們可以修改它~
現在Rectangle
的大小是sizeof(void*)
(指針大小), 即p->left
並不在這個對象的內存裡, 因而常量方法的返回值可以不聲明const
。 這時客戶便可以通過這個返回的left
來修改對象私有成員了。 所以返回對象內部的指針,會導致常量方法的const
屬性被破壞。 根本原因在於C++的bitwise constness語法檢查風格。
注意如果
p
被聲明為Point
而非Point*
時,p->left
仍處於當前類的內存區域內, 編譯器會要求left() const
返回值為const int&
。
可能你已經注意到了,我們完全可以稍微改善一下上面的代碼來實現私有成員的寫保護。 既然是由於bitwise constness編譯器不提供類型檢查, 我們手動限制返回值為const
即可:
const int& left() const{ return p->left; }
很多情況下問題確實是這樣解決的,比如實現operator[]() const
時。 但下標運算符只是一個特例,一般情況我們是不會返回內部指針的。 因為返回的指針和擁有者對象具有同樣的生命周期, 返回的指針很容易被懸空,比如有一個返回Rectangle
的bounding
函數:
const Rectangle bounding();
我們希望獲得那個Rectangle
的left
,可能會這樣寫:
const int& left = bounding().left();
問題在哪裡呢?left
被懸空了,它並沒有保持left
的值! 因為bounding()
返回的對象沒有賦值給任何變量它是一個臨時對象。 臨時對象在語句執行後立即銷毀,那個私有成員的引用也將失效。