條款17:讓接口容易被正確使用,不易被誤用
Make interfaces easy to use correctly andhard to use incorrectly
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"
很多客戶錯誤都可以通過引入新類型來預防。確實,類型系統是你阻止那些不合適的代碼通過編譯的主要支持者。我們可以引入簡單的外覆類型來區別日,月和年,並將這些類型用於 Data 的構造函數。
struct Day { //Month和Year與之類似
explicit Day(int d) :val(d) {} :
intval;
};
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
一旦放置了正確的類型,限制其值有時候是通情達理的。例如,月僅有12個合法值,所以 Month 類型應該反映這一點。方法之一是用一個枚舉來表現月,但是枚舉不具備類型安全性。例如枚舉能被作為整數使用。一個安全的解決方案是預先確定合法的 Month 的集合:
class Month {
public:
static Month Jan() { return Month(1); } // 函數而非對象,返回有效月份
static Month Feb() { return Month(2); }
...
static Month Dec() { return Month(12); }
... // 其它成員函數
private:
explicit Month(int m); // 阻止生成新的月份,這是月份專屬數據
...
};
Date d(Month::Mar(), Day(30), Year(1995));
防止可能的客戶錯誤的另一個方法是限制類型內能夠做的事情,常見的限制是加上const。實際上,除非你有很棒的理由,否則就讓你的類型行為與內置類型保持一致。客戶已經知道像 int 這樣的類型如何表現,所以你應該努力使你的類型在合理的前提下有同樣的表現。例如,如果 a 和 b 是 int,給 a*b 賦值是非法的。
避免無端和內置類型不相容的真正原因是為了提供行為一致的接口。很少有特性比一致性更易於引出易於使用的接口,也很少有特性比不一致性更易於加劇接口的惡化。STL容器的接口在很大程度上(雖然並不完美)是一致的,而這使得它們相當易於使用。例如,每一種 STL 容器都有一個名為size的成員函數可以知道容器中有多少對象。與此對比的是 Java,在那裡你對數組使用length屬性,對String使用length方法,而對List卻要使用size方法,在 .NET 中,Array有一個名為Length的屬性,而ArrayList卻有一個名為Count的屬性。一些開發人員認為集成開發環境(IDEs)能補償這些瑣細的矛盾,但他們錯了。矛盾在開發者工作中強加的精神折磨是任何IDE都無法完全消除的。
· 促進正確使用的方法包括接口的一致性,以及與內置類型的行為兼容。
任何一個要求客戶記住某些事情的接口都是有錯誤使用傾向的,因為客戶可能忘記做那些事情。例如,條款13介紹的factory函數,它返回一個指向動態分配的 Investment 繼承體系中的對象的指針。
Investment* createInvestment();
為了避免資源洩漏,createInvestment返回的指針最後必須被刪除,但這就為至少兩種類型錯誤創造了機會:刪除指針失敗,或刪除同一個指針一次以上。
你可以將createInvestment的返回值存入一個類似auto_ptr 或tr1::shared_ptr 智能指針,從而將使用delete的職責交給智能指針,但仍忘記使用智能指針,不如讓factory函數在第一現場即返回一個智能指針:
std::tr1::shared_ptr<Investment>createInvestment();
這就從根本上強制客戶將返回值存入一個tr1::shared_ptr,幾乎完全消除了當底層的 Investment 對象不再使用的時候忘記刪除的可能性。
· 預防錯誤的方法包括創建新的類型,限定類型的操作,約束對象的值,以及消除客戶的資源管理職責。
假設從 createInvestment得到一個Investment*指針的客戶期望將這個指針傳給一個名為getRidOfInvestment的函數,而不是對它使用delete。tr1::shared_ptr 提供了一個需要兩個參數(被管理的指針、當引用計數變為零時要調用的deleter)的構造函數。這啟發我們創建一個以getRidOfInvestment 為deleter的null tr1::shared_ptr的方法:
std::tr1::shared_ptr<Investment> pInv(0,getRidOfInvestment);
這不會通過編譯。tr1::shared_ptr的構造函數堅決要求它的第一個參數應該是一個指針,而0不是一個指針,它是一個int。當然,它能轉型為一個指針,但那在當前情況下並不夠好,tr1::shared_ptr堅決要求一個真正的指針。用強制轉型解決這個問題,因此createInvestment的實現代碼看起來是這樣:
std::tr1::shared_ptr<Investment>createInvestment()
{
std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0),
getRidOfInvestment);
retVal =... ; // 令retVal指向正確對象
returnretVal;
}
tr1::shared_ptr的一個特別好的特性是它自動逐指針地使用deleter以消除另一種潛在的客戶錯誤——“cross-DLL問題。”這個問題發生在:一個對象在一個動態鏈接庫(dynamicallylinked 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應該被使用”的跟蹤。
tr1::shared_ptr是一個消除某些客戶錯誤的簡單方法,值得我們核計其使用成本。最通用的 tr1::shared_ptr 實現來自於 Boost,其shared_ptr的大小是原始指針的兩倍,以動態分配內存用於簿記用途和deleter專屬數據,當調用它的deleter時使用一個virtual函數來調用,並在多線程程序修改引用次數時蒙受線程同步化的額外開銷(你可以通過定義一個預處理符號來使多線程支持失效。)。在缺點方面,它比一個原始指針大且慢,而且要使用輔助動態內存。在許多應用程序中,這些附加的運行時開銷並不顯著,而對客戶錯誤的減少卻是每一個人都看得見的。
· 好的接口易於正確使用,而難以錯誤使用。你應該在你的所有接口中為這個特性努力。
· tr1::shared_ptr 支持自定義 deleter。這可以防止 cross-DLL 問題,能用於自動解鎖互斥體(mutex)等。
摘自 pandawuwyj的專欄