# Item 23: 用非成員非友元函數取代成員函數
作者:Scott Meyers
譯者:fatalerror99 (iTePub's Nirvana)
發布:http://blog.csdn.net/fatalerror99/
想象一個象征 web 瀏覽器的類。在大量的函數中,這樣一個類也許會提供清空已下載成分的緩存。清空已訪問 URLs 的歷史,以及從系統移除所有 cookies 的功能:
```
class WebBrowser {
public:
...
void clearCache();
void clearHistory();
void removeCookies();
...
};
```
很多用戶希望能一起執行全部這些動作,所以 WebBrowser 可能也會提供一個函數去這樣做:
```
class WebBrowser {
public:
...
void clearEverything(); // calls clearCache, clearHistory,
// and removeCookies
...
};
```
當然,這個功能也能通過非成員函數調用適當的成員函數來提供:
```
void clearBrowser(WebBrowser& wb)
{
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
```
那么哪個更好呢,成員函數 clearEverything 還是非成員函數 clearBrowser?
面性對象原則指出:數據和對它們進行操作的函數應該被綁定到一起,而且建議成員函數是更好的選擇。不幸的是,這個建議是不正確的。它產生于對面向對象是什么的一個誤解。面向對象原則指出數據應該盡可能被封裝。與直覺不同,成員函數 clearEverything 居然會造成比非成員函數 clearBrowser 更差的封裝性。此外,提供非成員函數允許 WebBrowser 相關功能的更大的包裝彈性,而且,可以獲得更少的編譯依賴和 WebBrowser 擴展性的增進。因而,在很多方面非成員方法比一個成員函數更好。理解它的原因是非常重要的。
我們將從封裝開始。如果某物被封裝,它被從視線中隱藏。越多的東西被封裝,就越少有東西能看見它。越少有東西能看見它,我們改變它的彈性就越大,因為我們的改變僅僅直接影響那些能看見我們變了什么的東西。某物的封裝性越強,那么我們改變它的能力就越強。這就是將封裝的價值評價為第一的原因:它為我們提供一種改變事情的彈性,而僅僅影響有限的客戶。
結合一個對象考慮數據。越少有代碼能看到數據(也就是說,訪問它),數據封裝性就越強,我們改變對象的數據的特性的自由也就越大,比如,數據成員的數量,它們的類型,等等。作為多少代碼能看到一塊數據的粗糙的尺度,我們可以計數能訪問那塊數據的函數的數量:越多函數能訪問它,數據的封裝性就越弱。
Item 22 說明了數據成員應該是 private 的,因為如果它們不是,就有無限量的函數能訪問它們。它們根本就沒有封裝。對于 private 數據成員,能訪問他們的函數的數量就是類的成員函數的數量加上友元函數的數量,因為只有成員和友元能訪問 private 成員。假設在一個成員函數(能訪問的不只是一個類的 private 數據,還有 private 函數,枚舉,typedefs,等等)和一個提供同樣功能的非成員非友元函數(不能訪問上述那些東西)之間有一個選擇,能獲得更強封裝性的選擇是非成員非友元函數,因為它不會增加能訪問類的 private 部分的函數的數量。這就解釋了為什么 clearBrowser(非成員非友元函數)比 clearEverything(成員函數)更可取:它能為 WebBrowser 獲得更強的封裝性。
在這一點,有兩件事值得注意。首先,這個論證只適用于非成員非友元函數。友元能像成員函數一樣訪問一個類的 private 成員,因此同樣影響封裝。從封裝的觀點看,選擇不是在成員和非成員函數之間,而是在成員函數和非成員非友元函數之間。(當然,封裝并不是僅有的觀點,Item 24 說明如果觀點來自隱式類型轉換,選擇就是在成員和非成員函數之間。)
需要注意的第二件事是,如果僅僅是為了關注封裝,則可以指出,一個函數是一個類的非成員并不意味著它不可以是另一個類的成員。這對于習慣了所有函數必須屬于類的語言(例如,Eiffel,Java,C#,等等)的程序員是一個適度的安慰。例如,我們可以使 clearBrowser 成為一個 utility 類的 static 成員函數。只要它不是 WebBrowser 的一部分(或友元),它就不會影響 WebBrowser 的 private 成員的封裝。
在 C++ 中,一個更自然的方法是使 clearBrowser 成為與 WebBrowser 在同一個 namespace(名字空間)中的非成員函數:
```
namespace WebBrowserStuff {
class WebBrowser { ... };
void clearBrowser(WebBrowser& wb);
...
}
```
相對于形式上的自然,這樣更適用于它。無論如何,因為名字空間(不像類)能展開到多個源文件中。這是很重要的,因為類似 clearBrowser 的函數是方便性函數。作為既不是成員也不是友元,他們沒有對 WebBrowser 進行專門的訪問,所以他們不能提供任何一種 WebBrowser 的客戶不能通過其它方法得到的功能。例如,如果 clearBrowser 不存在,客戶可以直接調用 clearCache,clearHistory 和 removeCookies 本身。
一個類似 WebBrowser 的類可以有大量的方便性函數,一些是書簽相關的,另一些打印相關的,還有一些是 cookie 管理相關的,等等。作為一個一般的慣例,多數客戶僅對這些方便性函數的集合中的一些感興趣。沒有理由讓一個只對書簽相關的方便性函數感興趣的客戶在編譯時依賴其它函數,例如,cookie 相關的方便性函數。分隔它們的直截了當的方法就是在一個頭文件中聲明書簽相關的方便性函數,在另一個不同的頭文件中聲明 cookie 相關的方便性函數,在第三個頭文件聲明打印相關的方便性函數,等等:
```
// header "webbrowser.h" — header for class WebBrowser itself
// as well as "core" WebBrowser-related functionality
namespace WebBrowserStuff {
class WebBrowser { ... };
... // "core" related functionality, e.g.
// non-member functions almost
// all clients need
}
// header "webbrowserbookmarks.h"
namespace WebBrowserStuff {
... // bookmark-related convenience
} // functions
// header "webbrowsercookies.h"
namespace WebBrowserStuff {
... // cookie-related convenience
} // functions
...
```
注意這里就像標準 C++ 庫組織得一樣嚴密。勝于有一個單獨的一體式的 <C++StandardLibrary> 頭文件包含 std namespace 中的所有東西,它們在許多頭文件中(例如,<vector>,<algorithm>,<memory>,等等),每一個都聲明了 std 中的一些機能。僅僅需要 vector 相關機能的客戶不需要 #include <memory>,不用 list 的客戶沒有必要 #include <list>。這就允許客戶在編譯時僅僅依賴他們實際使用的那部分系統。(參見 Item 31 對減少編譯依賴的其它方法的討論。)當機能來自一個類的成員函數時,用這種方法分割它是不可能的,因為一個類必須作為一個整體來定義,它不能四分五裂。
將所有方便性函數放入多個頭文件中——但是在一個 namespace 中——也意味著客戶能容易地擴充方便性函數的集合。他們必須做的全部就是在 namespace 中加入更多的非成員非友元函數。例如,如果一個 WebBrowser 的客戶決定寫一個關于下載圖像的方便性函數,他或她僅僅需要新建一個頭文件,包含那些函數在 WebBrowserStuff namespace 中的聲明。這個新的函數現在就像其它方便性函數一樣可用并被集成。這是類不能提供的另一個特性,因為類定義對于客戶是擴充封閉的。當然,客戶可以派生新類,但是派生類不能訪問基類中被封裝的(也就是說,private 的)成員,所以這樣的“擴充機能”只有二等身份。此外,就像 Item 7 中解釋的,不是所有的類都是作為基類設計的。
Things to Remember
* 用非成員非友元函數取代成員函數。這樣做可以提高封裝性,包裝彈性,和機能擴充性。
- 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 映射