# Item 18: 使接口易于正確使用,而難以錯誤使用
作者:Scott Meyers
譯者:fatalerror99 (iTePub's Nirvana)
發布:http://blog.csdn.net/fatalerror99/
C++ 被淹沒于接口中。函數接口、類接口、模板接口。每一個接口都意味著客戶的代碼和你的代碼互相影響。假設你在和通情達理的人打交道,那些客戶也想做好工作。他們想要正確使用你的接口。在這種情況下,如果他們犯了一個錯誤,就說明你的接口至少有部分是不完善的。在理想情況下,如果一個接口的一種嘗試的用法不符合客戶的預期,代碼將無法編譯,反過來,如果代碼可以編譯,那么它做的就是客戶想要的。
開發易于正確使用,而難以錯誤使用的接口需要你考慮客戶可能造成的各種錯誤。例如,假設你正在設計一個代表時間的類的構造函數:
```
class Date {
public:
Date(int month, int day, int year);
...
};
```
匆匆一看,這個接口似乎是合乎情理的(至少在美國),但是客戶可能很容易地造成兩種錯誤。首先,他們可能會以錯誤的順序傳遞參數:
```
Date d(30, 3, 1995); // Oops! Should be "3, 30" , not "30, 3"
```
第二,他們可能傳遞一個非法的代表月或日的數字:
```
Date d(2, 20, 1995); // Oops! Should be "3, 30" , not "2, 20"
```
(后面這個例子看上去好像沒什么,但是想想鍵盤上,2 就在 3 的旁邊,這種 "off by one" 類型的錯誤并不罕見。)
很多客戶錯誤都可以通過引入新的類型來預防。確實,類型系統是你阻止那些不合適的代碼通過編譯的主要支持者。在當前情況下,我們可以引入簡單的包裝類型來區別日,月和年,并將這些類型用于 Data 的構造函數。
```
struct Day { struct Month { struct Year {
explicit Day(int d) explicit Month(int m) explicit Year(int y)
:val(d) {} :val(m) {} :val(y){}
int val; int val; int val;
}; }; };
class Date {
public:
Date(const Month& m, const Day& d, const Year& y);
...
};
Date d(30, 3, 1995); // error! wrong types
Date d(Day(30), Month(3), Year(1995)); // error! wrong types
Date d(Month(3), Day(30), Year(1995)); // okay, types are correct
```
將日,月和年做成封裝數據的羽翼豐滿的類比上面的簡單地使用 struct 更好(參見 Item 22),但是即使是 struct 也足夠證明明智地引入新類型在阻止接口的錯誤使用方面能工作得非常出色。
只要放置了正確的類型,它往往能合理地限制那些類型的值。例如,月僅有 12 個合法值,所以 Month 類型應該反映這一點。做到這一點的一種方法是用一個枚舉來表現月,但是枚舉不像我們希望的那樣是類型安全(type-safe)的。例如,枚舉能被作為整數使用(參見 Item 2)。一個安全的解決方案是預先確定合法的 Month 的集合:
```
class Month {
public:
static Month Jan() { return Month(1); } // functions returning all valid
static Month Feb() { return Month(2); } // Month values; see below for
... // why these are functions, not
static Month Dec() { return Month(12); } // objects
... // other member functions
private:
explicit Month(int m); // prevent creation of new
// Month values
... // month-specific data
};
Date d(Month::Mar(), Day(30), Year(1995));
```
如果用函數代替對象來表現月的主意讓你感到驚奇,那可能是因為你忘了非局部靜態對象(non-local static objects)的初始化的可靠性是值得懷疑的。Item 4 能喚起你的記憶。
防止可能的客戶錯誤的另一個方法是限制對一個類型能夠做的事情。施加限制的一個普通方法就是加上 const。例如,Item 3 解釋了使 operator\* 的返回類型具有 const 資格是如何能夠防止客戶對用戶自定義類型犯下這樣的錯誤:
```
if (a * b = c) ... // oops, meant to do a comparison!
```
實際上,這僅僅是另一條使類型易于正確使用而難以錯誤使用的普遍方針的一種表現:除非你有很棒的理由,否則就讓你的類型的行為與內建類型保持一致。客戶已經知道像 int 這樣的類型如何表現,所以你應該努力使你的類型的表現無論何時都同樣合理。例如,如果 a 和 b 是 int,給 a\*b 賦值是非法的。所以除非有一個非常棒理由脫離這種表現,否則,對你的類型來說這樣做也應該是非法的。
避免和內建類型毫無理由的不相容的真正原因是為了提供行為一致的接口。很少有特性比一致性更易于引出易于使用的接口,也很少有特性比不一致性更易于引出令人郁悶的接口。STL 容器的接口在很大程度上(雖然并不完美)是一致的,而且這使得它們相當易于使用。例如,每一種 STL 容器都有一個名為 size 的成員函數可以知道容器中有多少對象。與此對比的是 Java,在那里你對數組使用 length 屬性,對 String 使用 length 方法,而對 List 卻要使用 size 方法,在 .NET 中,Array 有一個名為 Length 的屬性,而 ArrayList 卻有一個名為 Count 的屬性。一些開發人員認為集成開發環境(IDEs)能補償這些瑣細的矛盾,但他們錯了。矛盾在開發者工作中強加的精神折磨是任何 IDE 都無法完全消除的。
任何一個要求客戶記住某些事情的接口都是有錯誤使用傾向的,因為客戶可能忘記做那些事情。例如,Item 13 介紹了一個 factory 函數,它返回一個指向動態分配的 Investment 繼承體系中的對象的指針。
```
Investment* createInvestment(); // from Item 13; parameters omitted
// for simplicity
```
為了避免資源泄漏,createInvestment 返回的指針最后必須被刪除,但這就為至少兩種類型的客戶錯誤創造了機會:刪除指針失敗,或刪除同一個指針一次以上。
Item 13 展示了客戶可以怎樣將 createInvestment 的返回值存入一個類似 auto_ptr 或 tr1::shared_ptr 智能指針,從而將使用 delete 的職責交給智能指針。但是如果客戶忘記使用智能指針呢?在很多情況下,一個更好的接口會預先判定將要出現的問題,從而讓 factory 函數在第一現場即返回一個智能指針:
```
std::tr1::shared_ptr<Investment> createInvestment();
```
這就從根本上強制客戶將返回值存入一個 tr1::shared_ptr,幾乎完全消除了當底層的 Investment 對象不再使用的時候忘記刪除的可能性。
實際上,返回一個 tr1::shared_ptr 使得接口的設計者預防許多其它客戶的與資源泄漏相關的錯誤成為可能,因為,就像 Item 14 解釋的:當一個智能指針被創建的時候,tr1::shared_ptr 允許將一個資源釋放(resource-release)函數——一個 "deleter" ——綁定到智能指針上。(auto_ptr 則沒有這個能力。)
假設從 createInvestment 得到一個 Investment\* 指針的客戶期望將這個指針傳給一個名為 getRidOfInvestment 的函數,而不是對它使用 delete。這樣一個接口又為一種新的客戶錯誤打開了門,這就是客戶可能使用了錯誤的資源析構機制(也就是說,用了 delete 而不是 getRidOfInvestment)。createInvestment 的實現可以通過返回一個在它的 deleter 上綁定了 getRidOfInvestment 的 tr1::shared_ptr 來預防這個問題。
tr1::shared_ptr 提供了一個需要兩個參數(要被管理的指針和當引用計數變為零時要調用的 deleter)的構造函數。這里展示了創建一個以 getRidOfInvestment 為 deleter 的 null tr1::shared_ptr 的方法:
```
std::tr1::shared_ptr<Investment> // attempt to create a null
pInv(0, getRidOfInvestment); // shared_ptr with a custom deleter;
// this won't compile
```
唉,這不是合法的 C++。tr1::shared_ptr 的構造函數堅決要求它的第一個參數應該是一個指針,而 0 不是一個指針,它是一個 int。當然,它能轉型為一個指針,但那在當前情況下并不夠好用,tr1::shared_ptr 堅決要求一個真正的指針。用強制轉型解決這個問題:
```
std::tr1::shared_ptr<Investment> // create a null shared_ptr with
pInv(static_cast<Investment*>(0), // getRidOfInvestment as its
getRidOfInvestment); // deleter; see Item 27 for info on
// static_cast
```
據此,實現返回一個以 getRidOfInvestment 作為 deleter 的 tr1::shared_ptr 的 createInvestment 的代碼看起來就像這個樣子:
```
std::tr1::shared_ptr<Investment> createInvestment()
{
std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0),
getRidOfInvestment);
retVal = ... ; // make retVal point to the
// correct object
return retVal;
}
```
當然,如果將被 pInv 管理的裸指針可以在創建 pInv 時被確定,最好是將這個裸指針傳給 pInv 的構造函數,而不是將 pInv 初始化為 null 然后再賦值給它。至于方法上的細節,參考 Item 26。
tr1::shared_ptr 的一個特別好的特性是它自動逐指針地使用 deleter 以消除另一種潛在的客戶錯誤——“cross-DLL 問題。”這個問題發生在這種情況下:一個對象在一個動態鏈接庫(dynamically linked library (DLL))中通過 new 被創建,在另一個不同的 DLL 中被刪除。在許多平臺上,這樣的 cross-DLL new/delete 對會引起運行時錯誤。tr1::shared_ptr 可以避免這個問題,因為它的缺省的 deleter 只將 delete 用于這個 tr1::shared_ptr 被創建的 DLL 中。這就意味著,例如,如果 Stock 是一個繼承自 Investment 的類,而且 createInvestment 被實現如下,
```
std::tr1::shared_ptr<Investment> createInvestment()
{
return std::tr1::shared_ptr<Investment>(new Stock);
}
```
返回的 tr1::shared_ptr 能在 DLL 之間進行傳遞,而不必關心 cross-DLL 問題。指向這個 Stock 的 tr1::shared_ptr 將保持對“當這個 Stock 的引用計數變為零的時候,哪一個 DLL 的 delete 應該被使用”的跟蹤。
這個 Item 不是關于 tr1::shared_ptr 的——而是關于使接口易于正確使用,而難以錯誤使用的——但 tr1::shared_ptr 正是這樣一個消除某些客戶錯誤的簡單方法,值得用一個概述來看看使用它的代價。最通用的 tr1::shared_ptr 實現來自于 Boost(參見 Item 55)。Boost 的 shared_ptr 的大小是裸指針的兩倍,將動態分配內存用于簿記和 deleter 專用(deleter-specific)數據,當調用它的 deleter 時使用一個虛函數來調用,在一個它認為是多線程的應用程序中,當引用計數被改變,會導致線程同步開銷。(你可以通過定義一個預處理符號來使多線程支持失效。)在缺點方面,它比一個裸指針大,比一個裸指針慢,而且要使用輔助的動態內存。在許多應用程序中,這些附加的運行時開銷并不顯著,而對客戶錯誤的減少卻是每一個人都看得見的。
Things to Remember
* 好的接口易于正確使用,而難以錯誤使用。你應該在你的所有接口中為這個特性努力。
* 使易于正確使用的方法包括在接口和行為兼容性上與內建類型保持一致。
* 預防錯誤的方法包括創建新的類型,限定類型的操作,約束對象的值,以及消除客戶的資源管理職責。
* tr1::shared_ptr 支持自定義 deleter。這可以防止 cross-DLL 問題,能用于自動解鎖互斥體(參見 Item 14)等。
- 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 映射