條款18 讓介面容易被正確使用,不易被誤用

Sunshine_top發表於2015-03-12

總結:

1、好的介面很容易被正確使用,不容易被誤用。你應該在你的所有介面中努力達成這些性質。

2、促進正確使用的方法包括介面的一致性,以及與內建型別的行為相容。

3、預防錯誤的方法包括建立新的型別,限定型別的操作,約束物件的值,以及消除客戶的資源管理職責。

4、tr1::shared_ptr 支援自定義 deleter。這可以防止 cross-DLL 問題,能用於自動解鎖互斥體(mutex)等。


        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函式來呼叫,並在多執行緒程式修改引用次數時蒙受執行緒同步化的額外開銷(你可以通過定義一個預處理符號來使多執行緒支援失效。)。在缺點方面,它比一個原始指標大且慢,而且要使用輔助動態記憶體。在許多應用程式中,這些附加的執行時開銷並不顯著,而對客戶錯誤的減少卻是每一個人都看得見的。




相關文章