Effective c++ Item 28 不要返回指向對象內部數據(internals)的句柄(handles)。本站提示廣大學習愛好者:(Effective c++ Item 28 不要返回指向對象內部數據(internals)的句柄(handles))文章只能為提供參考,不一定能成為您想要的結果。以下是Effective c++ Item 28 不要返回指向對象內部數據(internals)的句柄(handles)正文
假設你正在操作一個Rectangle類。每個矩形可以通過左上角的點和右下角的點來表示。為了保證一個Rectangle對象盡可能小,你可能決定不把定義矩形范圍的點存儲在Rectangle類中,而是把它放入一個輔助結構體中,Rectangle中聲明一個指向它的指針就可以了:
1 class Point { // class for representing points 2 3 public: 4 5 Point(int x, int y); 6 7 ... 8 9 void setX(int newVal); 10 11 void setY(int newVal); 12 13 ... 14 15 }; 16 17 18 19 struct RectData { // Point data for a Rectangle 20 21 Point ulhc; // ulhc = “ upper left-hand corner” 22 23 Point lrhc; // lrhc = “ lower right-hand corner” 24 25 }; 26 27 class Rectangle { 28 29 ... 30 31 private: 32 33 std::tr1::shared_ptr<RectData> pData; // see Item 13 for info on 34 35 36 37 }; // tr1::shared_ptr
1. 由返回指向對象內部數據的引用所引發的兩個問題 1.1 問題分析
因為Rectangle的客戶需要能夠獲知一個矩形的范圍,類因此提供了upperLeft和lowerRight函數。然而,Point是一個自定義的類型,所以你需要留意Item 20:對於用戶自定義類型,按引用傳遞比按值傳遞更高效,這些函數返回了對底層Point對象的引用:
1 class Rectangle { 2 3 public: 4 5 ... 6 7 Point& upperLeft() const { return pData->ulhc; } 8 9 Point& lowerRight() const { return pData->lrhc; } 10 11 ... 12 13 };
這種設計可以編譯通過,但卻是錯誤的。事實上,它是自相矛盾的。一方面,upperLeft和lowerRight被聲明成const成員函數,因為它們只用來為客戶提供獲知矩陣包含哪些點的方法,並沒有讓客戶修改矩陣(Item 3)。另一方面,兩個函數都返回指向私有內部數據的引用——而此引用能夠被調用者用來修改內部數據!舉個例子:
1 Point coord1(0, 0); 2 3 Point coord2(100, 100); 4 5 const Rectangle rec(coord1, coord2); 6 7 // rec is a const rectangle from 8 9 10 // (0, 0) to (100, 100) 11 rec.upperLeft().setX(50); // now rec goes from 12 // (50, 0) to (100, 100)!
注意upperLeft的調用者是如何利用返回的指向rec內部的Point數據成員的引用來修改這個成員的。但是rec是const變量。
我們能從中學到兩點。首先,一個數據成員的封裝性同以這個數據成員的引用作為返回值的可訪問級別最高(most accessible)的成員函數一致。在這種情況下,雖然ulhc和lrhc對於Rectangle來說是private的,它們實際上是public的,因為public函數upperLeft和lowerRight返回了指向它們的引用。第二,如果一個const成員函數返回指向數據的引用,而此數據被存儲在當前對象之外,那麼函數的調用者就能夠修改這些數據。(這是bitwise constness局限性的附帶結果 Item 3)。
1.2 句柄(handles)不僅包含引用,也包含指針和迭代器我們討論的都是返回引用的成員函數,但是如果它們返回指針或者迭代器,同樣原因導致的同樣問題也將會存在。引用,指針和迭代器都是句柄(handles),返回一個指向對象內部數據的句柄常常有破壞封裝型的風險。也會導致從const成員函數傳遞出去的對象的狀態被修改掉。
1.3 內部數據(internals data)不僅包含數據成員,也包括成員函數我們通常認為一個對象的“內部數據”只針對數據成員,但非public的成員函數也是對象內部數據的一部分。因此,禁止返回指向它們的句柄同樣重要。這意味著絕不要從成員函數中返回一個指向更低訪問級別的函數的指針。如果你這麼做了,有效的訪問級別就是訪問級別更高的那個函數,因為客戶可以獲得訪問級別更低的函數的指針,然後通過指針來調用此函數。
2. 解決上面兩問題的方法,為引用添加const
返回指向成員函數指針的函數並不普通,讓我們重新關注Rectangle類和它的upperLeft和lowerRight成員函數。我們發現的兩個問題可以通過簡單的為其返回值添加const來消除:
1 class Rectangle { 2 public: 3 ... 4 const Point& upperLeft() const { return pData->ulhc; } 5 const Point& lowerRight() const { return pData->lrhc; } 6 ... 7 };
使用這個修改後的設計,客戶可以讀取定義一個矩形的點,但是不能修改它們。這意味著upperLeft和lowerRight的聲明不再是一個謊言,因為它們不再允許調用者修改對象的狀態。對於封裝問題,我們的意圖是讓客戶能夠看到組成矩形的點,因此我們故意放松了封裝型。更加重要的是,這是有局限性的“放松“:這些函數是只讀的。
3. 返回const引用會引入新的問題即便如此,upperLeft和lowerRight仍然返回了指向對象內部數據的句柄,這可能會在其它方面出現問題。特別是它能導致懸掛指針:指向部分對象的句柄不再存在。這種對象消失問題的最常見的根源在於按值返回的函數。舉個例子,考慮一個為GUI對象返回邊界框的函數,邊界框用矩陣表示:
1 class GUIObject { ... }; 2 3 const Rectangle // returns a rectangle by 4 5 boundingBox(const GUIObject& obj); // value; see Item 3 for why 6 7 8 // return type is const
現在考慮一個客戶如果使用這個函數:
1 GUIObject *pgo; // make pgo point to 2 ... // some GUIObject 3 const Point *pUpperLeft = // get a ptr to the upper 4 &(boundingBox(*pgo).upperLeft()); // left point of its 5 // bounding box
這個對boundingBox的調用將返回一個新的臨時Rectangle對象.這個對象沒有名字,我們叫它temp。upperLeft將在temp上被調用,這個調用返回了指向temp內部數據的引用,temp內部數據就是組成矩形的一個Point。pUpperLeft將會指向這個Ponit對象。到現在為止一切正常,但是還沒完呢,因為在聲明結束時,boundingBox的返回值——temp——將會被銷毀,這將間接導致temp的Point對象被析構。這樣使得pUpperLeft指向一個不再存在的對象;pUpperLeft在創建它的語句結束時變成了懸掛指針!
這也是為什麼任何返回指向對象內部數據的句柄的函數都是危險的。不論這個句柄是一個指針,或者一個函數,或者一個迭代器。也不管這個函數有沒有被聲明為const。也不管成員函數返回的句柄是否為const。問題的關鍵在於函數有沒有返回句柄,因為一旦返回了,就會有句柄比其指向的對象存在時間更長的危險。
4. 例外的情況這並不意味著你永遠不應該讓一個成員函數返回一個句柄。有時候你必須這麼做。舉個例子,operator[]允許你從string或者vector中將單個元素摘出來,這些operator[]返回的就是指向容器內部數據的引用(Item 3)——容器被銷毀的時候,它裡面的數據也被銷毀。但這樣的函數只是一個例外。
5. 總結避免返回指向對象內部數據的句柄(引用,指針或者迭代器)。不返回句柄可以增強封裝性,幫助const成員函數的行為為真正的const,減少懸掛指針被創建的可能。