讀書筆記 effective c++ Item 18 使接口容易被正確使用,不容易被誤用。本站提示廣大學習愛好者:(讀書筆記 effective c++ Item 18 使接口容易被正確使用,不容易被誤用)文章只能為提供參考,不一定能成為您想要的結果。以下是讀書筆記 effective c++ Item 18 使接口容易被正確使用,不容易被誤用正文
C++中充斥著接口:函數接口,類接口,模板接口。每個接口都是客戶同你的代碼進行交互的一種方法。假設你正在面對的是一些“講道理”的人員,這些客戶嘗試把工作做好,他們希望能夠正確使用你的接口。在這種情況下,如果接口被誤用,你的接口應該至少負一部分的責任。理想情況下,如果使用一個接口沒有做到客戶希望做到的,代碼應該不能通過編譯;如果代碼通過了編譯,那麼它就能做到客戶想要的。
2. 編寫好的接口的方法列舉 2.1 使接口不容易被誤用——通過引入新的類型開發出容易被正確使用不容易被誤用的接口需要你考慮客戶可能出現的所有類型的錯誤。舉個例子,假設你正在為一個表示日期的類設計一個構造函數:
1 class Date { 2 3 public: 4 5 Date(int month, int day, int year); 6 7 ... 8 9 };
乍一看,這個接口可能看上去去合理的,但是客戶很容易犯至少兩種錯誤。
第一,他們可能搞錯參數的傳遞順序:
1 Date d(30, 3, 1995); // Oops! Should be “3, 30” , not “30, 3”
第二,他們可能傳遞一個無效的月份或者天數(day number):
1 Date d(3, 40, 1995); // Oops! Should be “3, 30” , not “3, 40”
(最後一個例子看上去很病態,但是不要忘了在鍵盤上,數字4和3是挨著的,將3錯打成4這樣的錯誤不是不常見。)
通過引入新的類型,許多客戶錯誤就能被避免。確實,類型系統(type system)是你阻止不合要求的代碼編譯通過的主要盟友。在這種情況下,我們可以引入簡單的包裝類型來區分天,月和年,然後在Date構造函數中使用這些類型:
1 struct Day{ 2 explicit Day(int d): val(d) {} 3 int val; 4 }; 5 struct Month { 6 explicit Month(int m): val(m) {} 7 int val; 8 }; 9 struct Year { 10 explicit Year(int y): val(y){} 11 int val; 12 }; 13 class Date { 14 public: 15 Date(const Month& m, const Day& d, const Year& y); 16 ... 17 }; 18 Date d(30, 3, 1995); // error! wrong types 19 Date d(Day(30), Month(3), Year(1995)); // error! wrong types 20 Date d(Month(3), Day(30), Year(1995)); // okay, types are correct
將Day,Month和Year數據封裝在羽翼豐滿的類中比上面簡單的使用struct要更好(Item 22),但是使用struct就足以證明,明智的引入新類型可以很好的阻止接口被誤用的問題。
一旦正確的類型准備好了,就能夠合理的約束這些類型的值。舉個例子,只有12個月份應該能夠通過Month類型反映出來。一種方法是使用一個枚舉類型來表示月份,但是枚舉不是我們喜歡的類型安全的類型。例如,枚舉可以像int一樣使用(Item 2)。一個更加安全的解決方案是預先將所有有效的月份都定義出來。
1 class Month { 2 3 public: 4 5 static Month Jan() { return Month(1); } // functions returning all valid 6 7 static Month Feb() { return Month(2); } // Month values; see below for 8 9 ... // why these are functions, not 10 11 static Month Dec() { return Month(12); } // objects 12 13 ... // other member functions 14 15 private: 16 17 explicit Month(int m); // prevent creation of new 18 19 // Month values 20 21 ... // month-specific data 22 23 }; 24 25 Date d(Month::Mar(), Day(30), Year(1995));
如果使用函數代替對象來表示指定月份值會讓你覺的奇怪的話,可能是因為你忘記了非本地static對象的初始化是有問題的(見 Item 4)。
2.2 使接口不容易被誤用——對類型的操作進行限定另外一種防止類似錯誤的方法是對類型能夠做什麼進行限制。進行限制的一般方法是添加const。舉個例子,Item 3解釋了對於用戶自定義的類型,把operator*的返回類型加上const能夠防止下面錯誤的發生:
1 if (a * b = c) ... // oops, meant to do a comparison!
2.3 使接口容易被正確使用——提供行為一致的接口
事實上,這只是“使類型容易正確使用不容易被誤用”的另外一個指導方針的表現形式:除非有更好的理由,讓你的自定義類型同內建類型的行為表現一致。客戶已經知道像int一樣的內建類型的行為是什麼樣子的,所以在任何合理的時候你應該努力使你的類型表現與其一致。舉個例子,如果a和b是int類型,那麼賦值給a*b是不合法的,所以除非有一個好的理由偏離這種行為,你應該使你的類型同樣不合法。每當你不確定自定義類型的行為時,按照int來做就可以了。
防止自定義類型同內建類型無端不兼容的真正原因是提供行為一致的接口。沒有特征比“一致性”更能使接口容易被使用了,也沒有特征比“不一致性”更加導致接口容易被誤用了。STL容器的接口大體上(雖然不是完全一致)是一致的,這使得它們使用起來相當容易。舉個例子,每個STL容易有一個size成員函數,用來指出容器中的對象數量。與Java相比,arrays使用length屬性(property)來表示對象數量,而String使用length方法(method)來表示,List使用size方法來表示;對於.NET來說,Array有一個Length屬性,而ArrayList有一個Count屬性。一些開發人員認為集成開發環境(IDE)使這種不一致性不再重要,但他們錯了。不一致性會將精神摩擦強加到開發人員的工作中,沒有任何IDE能夠將其擦除。
2.4 使接口不容易被誤用——使用shared_ptr消除客戶管理資源的責任 2.4.1 讓函數返回一個智能指針一個要讓客戶記住做某事的接口比較容易被用錯,因為客戶有可能會忘記做。舉個例子,Item 13中引入一個工廠函數,在一個Investment繼承體系中返回指向動態分配內存的指針:
1 Investment* createInvestment(); // from Item 13; parameters omitted 2 3 // for simplicity
為了防止資源洩漏,createInvesment返回的指針最後必須被delete,但是這為至少兩類客戶錯誤的出現創造了機會:delete指針失敗,多次delete同一個指針。
Item 13展示了客戶如何將createInvestment的返回值存入像auto_ptr或者tr1::shared_ptr一樣的智能指針中,這樣就將delete的責任交給智能指針。但是如果客戶忘記使用智能指針該怎麼辦?在許多情況下,更好的接口是要先發制人,讓函數首先返回一個智能指針:
1 std::tr1::shared_ptr<Investment> createInvestment();
這就強制客戶將返回值保存在tr1::shared_ptr中,從而完全消除了忘記delete不再被使用的底層Investment對象的可能性。
2.4.2 返回綁定刪除器的智能指針事實上,對於一個接口設計者來說,返回tr1::shared_ptr能夠避免許多其他的有關資源釋放的客戶錯誤,因為Item 14中解釋道,在創建智能指針時,tr1::shared_ptr允許將一個資源釋放函數——釋放器(deleter)——綁定到智能指針上。
假設客戶從createInvestment得到一個Investment*指針,我們通過將這個指針傳遞給一個叫做getRidOfInvestment的函數來釋放資源而不是直接使用delete。這樣的接口開啟了另外一類客戶錯誤的大門:客戶可能會使用錯誤的資源析構機制(用delete而不是用提供的getRidOfInvestment接口)。createInvestment的實現者可以先發制人,返回一個tr1::shared_ptr,並將getRidOfInvestment綁定為刪除器。
Tr1::shared_ptr提供了一個有兩個參數的構造函數:需要被管理的指針和當引用計數為0時需要被調用的刪除器。這就提供了一個創建用getRidOfInvestment作為刪除器的空tr1::shared_ptr的方法:
1 std::tr1::shared_ptr<Investment> // attempt to create a null 2 3 pInv(0, getRidOfInvestment); // shared_ptr with a custom deleter; 4 5 // this won’t compile
上面不是有效的c++,tr1::shared_ptr構造函數的第一個參數必須為指針,但是0不是指針。雖然它可以轉換成指針,但是在這個例子中不夠好;tr1::shared_ptr堅持使用真實的指針。一個cast就能解決問題:
1 std::tr1::shared_ptr<Investment> // create a null shared_ptr with 2 3 pInv( static_cast<Investment*>(0), // getRidOfInvestment as its 4 5 getRidOfInvestment); // deleter; see Item 27 for info on 6 7 // static_cast
這意味著實現一個createInvestment的代碼如下(返回值為綁定了getRidOfInvestment作為刪除器的tr1::shared_ptr):
1 std::tr1::shared_ptr<Investment> createInvestment() 2 3 { 4 5 std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0), 6 7 getRidOfInvestment); 8 9 ... // make retVal point to the 10 11 // correct object 12 13 return retVal; 14 15 }
當然,如果在創建一個retVal之前就能夠決定一個原生指針是不是由reVal來管理,將原生指針直接傳遞給retVal的構造函數比先將retVal初始化為null然後做一個賦值操作要好。為什麼請看 Item 26。
2.5 使用智能指針消除交叉-DLL錯誤Tr1::shared_ptr的一個特別好的性質是它可以用它的刪除器來消除另外一個客戶錯誤——交叉(cross)-DLL錯誤。當一個對象在一個DLL中使用new被創建,但是在另外一個DLL中被delete時這個問題就會出現。在許多平台中,這樣的交叉-DLL new/delete對會導致運行時錯誤。使用tr1::shared_ptr可以避免這種錯誤,因為它使用的默認的刪除器來自創建tr1::shared_ptr的DLL。這就意味著,例如,如果Stock是一個繼承自Investment的類,createInvestment實現如下:
1 std::tr1::shared_ptr<Investment> createInvestment() 2 3 { 4 5 return std::tr1::shared_ptr<Investment>(new Stock); 6 7 }
返回的tr1::shared_ptr可以在DLL之間被傳遞而不用考慮cross-DLL問題。在Stock的引用計數為0的時候,指向Stock的tr1::shared_ptr指針會追蹤哪個DLL的刪除器被用來釋放資源。
3.使用智能指針的代價這個Item不是關於tr1::shared_ptr的——它是關於“使接口容易被正確使用不容易被誤用”這個議題的——但是使用tr1::shared_ptr是一個如此容易的消除客戶錯誤的方法,所以值得將使用它的代價做一個概述。Tr1::shared_ptr的最一般的實現來自Boost(Item 55)。Boost中的shared_ptr占用內存是原生指針的兩倍,為bookkeeping(引用計數)和deleter-specific(專屬刪除器) 數據分配動態內存,調用刪除器的時候使用虛函數,當在一個應用中修改引用計數時,如果它認為自己是多線程的,會引發線程同步開銷。(你可以通過定義一個預處理符號來disable多線程支持)一句話,它比原生指針占用內存多,比原生指針慢,並且使用了輔助的動態內存。但是在許多應用中,這些額外的運行時開銷是不明顯的,但是客戶錯誤的消除對每個人來說都是顯而易見的。
4.總結