读书笔记 effective c++ Item 18 使接口容易被正确使用,不容易被误用
1. 什么樣的接口才是好的接口
C++中充斥著接口:函數(shù)接口,類接口,模板接口。每個(gè)接口都是客戶同你的代碼進(jìn)行交互的一種方法。假設(shè)你正在面對的是一些“講道理”的人員,這些客戶嘗試把工作做好,他們希望能夠正確使用你的接口。在這種情況下,如果接口被誤用,你的接口應(yīng)該至少負(fù)一部分的責(zé)任。理想情況下,如果使用一個(gè)接口沒有做到客戶希望做到的,代碼應(yīng)該不能通過編譯;如果代碼通過了編譯,那么它就能做到客戶想要的。
2. 編寫好的接口的方法列舉
2.1 使接口不容易被誤用——通過引入新的類型
開發(fā)出容易被正確使用不容易被誤用的接口需要你考慮客戶可能出現(xiàn)的所有類型的錯(cuò)誤。舉個(gè)例子,假設(shè)你正在為一個(gè)表示日期的類設(shè)計(jì)一個(gè)構(gòu)造函數(shù):
1 class Date { 2 3 public: 4 5 Date(int month, int day, int year); 6 7 ... 8 9 };乍一看,這個(gè)接口可能看上去去合理的,但是客戶很容易犯至少兩種錯(cuò)誤。
第一,他們可能搞錯(cuò)參數(shù)的傳遞順序:
1 Date d(30, 3, 1995); // Oops! Should be “3, 30” , not “30, 3”?第二,他們可能傳遞一個(gè)無效的月份或者天數(shù)(day number):
1 Date d(3, 40, 1995); // Oops! Should be “3, 30” , not “3, 40”(最后一個(gè)例子看上去很病態(tài),但是不要忘了在鍵盤上,數(shù)字4和3是挨著的,將3錯(cuò)打成4這樣的錯(cuò)誤不是不常見。)
通過引入新的類型,許多客戶錯(cuò)誤就能被避免。確實(shí),類型系統(tǒng)(type system)是你阻止不合要求的代碼編譯通過的主要盟友。在這種情況下,我們可以引入簡單的包裝類型來區(qū)分天,月和年,然后在Date構(gòu)造函數(shù)中使用這些類型:
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數(shù)據(jù)封裝在羽翼豐滿的類中比上面簡單的使用struct要更好(Item 22),但是使用struct就足以證明,明智的引入新類型可以很好的阻止接口被誤用的問題。
一旦正確的類型準(zhǔn)備好了,就能夠合理的約束這些類型的值。舉個(gè)例子,只有12個(gè)月份應(yīng)該能夠通過Month類型反映出來。一種方法是使用一個(gè)枚舉類型來表示月份,但是枚舉不是我們喜歡的類型安全的類型。例如,枚舉可以像int一樣使用(Item 2)。一個(gè)更加安全的解決方案是預(yù)先將所有有效的月份都定義出來。
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));?
如果使用函數(shù)代替對象來表示指定月份值會讓你覺的奇怪的話,可能是因?yàn)槟阃浟朔潜镜豷tatic對象的初始化是有問題的(見 Item 4)。
2.2 使接口不容易被誤用——對類型的操作進(jìn)行限定
另外一種防止類似錯(cuò)誤的方法是對類型能夠做什么進(jìn)行限制。進(jìn)行限制的一般方法是添加const。舉個(gè)例子,Item 3解釋了對于用戶自定義的類型,把operator*的返回類型加上const能夠防止下面錯(cuò)誤的發(fā)生:
1 if (a * b = c) ... // oops, meant to do a comparison!?
2.3 使接口容易被正確使用——提供行為一致的接口
事實(shí)上,這只是“使類型容易正確使用不容易被誤用”的另外一個(gè)指導(dǎo)方針的表現(xiàn)形式:除非有更好的理由,讓你的自定義類型同內(nèi)建類型的行為表現(xiàn)一致。客戶已經(jīng)知道像int一樣的內(nèi)建類型的行為是什么樣子的,所以在任何合理的時(shí)候你應(yīng)該努力使你的類型表現(xiàn)與其一致。舉個(gè)例子,如果a和b是int類型,那么賦值給a*b是不合法的,所以除非有一個(gè)好的理由偏離這種行為,你應(yīng)該使你的類型同樣不合法。每當(dāng)你不確定自定義類型的行為時(shí),按照int來做就可以了。
防止自定義類型同內(nèi)建類型無端不兼容的真正原因是提供行為一致的接口。沒有特征比“一致性”更能使接口容易被使用了,也沒有特征比“不一致性”更加導(dǎo)致接口容易被誤用了。STL容器的接口大體上(雖然不是完全一致)是一致的,這使得它們使用起來相當(dāng)容易。舉個(gè)例子,每個(gè)STL容易有一個(gè)size成員函數(shù),用來指出容器中的對象數(shù)量。與Java相比,arrays使用length屬性(property)來表示對象數(shù)量,而String使用length方法(method)來表示,List使用size方法來表示;對于.NET來說,Array有一個(gè)Length屬性,而ArrayList有一個(gè)Count屬性。一些開發(fā)人員認(rèn)為集成開發(fā)環(huán)境(IDE)使這種不一致性不再重要,但他們錯(cuò)了。不一致性會將精神摩擦強(qiáng)加到開發(fā)人員的工作中,沒有任何IDE能夠?qū)⑵洳脸?/p>
2.4 使接口不容易被誤用——使用shared_ptr消除客戶管理資源的責(zé)任
2.4.1 讓函數(shù)返回一個(gè)智能指針
一個(gè)要讓客戶記住做某事的接口比較容易被用錯(cuò),因?yàn)榭蛻粲锌赡軙涀觥Ee個(gè)例子,Item 13中引入一個(gè)工廠函數(shù),在一個(gè)Investment繼承體系中返回指向動態(tài)分配內(nèi)存的指針:
1 Investment* createInvestment(); // from Item 13; parameters omitted 2 3 // for simplicity為了防止資源泄漏,createInvesment返回的指針最后必須被delete,但是這為至少兩類客戶錯(cuò)誤的出現(xiàn)創(chuàng)造了機(jī)會:delete指針失敗,多次delete同一個(gè)指針。
Item 13展示了客戶如何將createInvestment的返回值存入像auto_ptr或者tr1::shared_ptr一樣的智能指針中,這樣就將delete的責(zé)任交給智能指針。但是如果客戶忘記使用智能指針該怎么辦?在許多情況下,更好的接口是要先發(fā)制人,讓函數(shù)首先返回一個(gè)智能指針:
1 std::tr1::shared_ptr<Investment> createInvestment();這就強(qiáng)制客戶將返回值保存在tr1::shared_ptr中,從而完全消除了忘記delete不再被使用的底層Investment對象的可能性。
2.4.2 返回綁定刪除器的智能指針
事實(shí)上,對于一個(gè)接口設(shè)計(jì)者來說,返回tr1::shared_ptr能夠避免許多其他的有關(guān)資源釋放的客戶錯(cuò)誤,因?yàn)镮tem 14中解釋道,在創(chuàng)建智能指針時(shí),tr1::shared_ptr允許將一個(gè)資源釋放函數(shù)——釋放器(deleter)——綁定到智能指針上。
?
假設(shè)客戶從createInvestment得到一個(gè)Investment*指針,我們通過將這個(gè)指針傳遞給一個(gè)叫做getRidOfInvestment的函數(shù)來釋放資源而不是直接使用delete。這樣的接口開啟了另外一類客戶錯(cuò)誤的大門:客戶可能會使用錯(cuò)誤的資源析構(gòu)機(jī)制(用delete而不是用提供的getRidOfInvestment接口)。createInvestment的實(shí)現(xiàn)者可以先發(fā)制人,返回一個(gè)tr1::shared_ptr,并將getRidOfInvestment綁定為刪除器。
Tr1::shared_ptr提供了一個(gè)有兩個(gè)參數(shù)的構(gòu)造函數(shù):需要被管理的指針和當(dāng)引用計(jì)數(shù)為0時(shí)需要被調(diào)用的刪除器。這就提供了一個(gè)創(chuàng)建用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構(gòu)造函數(shù)的第一個(gè)參數(shù)必須為指針,但是0不是指針。雖然它可以轉(zhuǎn)換成指針,但是在這個(gè)例子中不夠好;tr1::shared_ptr堅(jiān)持使用真實(shí)的指針。一個(gè)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這意味著實(shí)現(xiàn)一個(gè)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 }?
當(dāng)然,如果在創(chuàng)建一個(gè)retVal之前就能夠決定一個(gè)原生指針是不是由reVal來管理,將原生指針直接傳遞給retVal的構(gòu)造函數(shù)比先將retVal初始化為null然后做一個(gè)賦值操作要好。為什么請看 Item 26。
2.5 使用智能指針消除交叉-DLL錯(cuò)誤
Tr1::shared_ptr的一個(gè)特別好的性質(zhì)是它可以用它的刪除器來消除另外一個(gè)客戶錯(cuò)誤——交叉(cross)-DLL錯(cuò)誤。當(dāng)一個(gè)對象在一個(gè)DLL中使用new被創(chuàng)建,但是在另外一個(gè)DLL中被delete時(shí)這個(gè)問題就會出現(xiàn)。在許多平臺中,這樣的交叉-DLL new/delete對會導(dǎo)致運(yùn)行時(shí)錯(cuò)誤。使用tr1::shared_ptr可以避免這種錯(cuò)誤,因?yàn)樗褂玫哪J(rèn)的刪除器來自創(chuàng)建tr1::shared_ptr的DLL。這就意味著,例如,如果Stock是一個(gè)繼承自Investment的類,createInvestment實(shí)現(xiàn)如下:
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的引用計(jì)數(shù)為0的時(shí)候,指向Stock的tr1::shared_ptr指針會追蹤哪個(gè)DLL的刪除器被用來釋放資源。
3.使用智能指針的代價(jià)
這個(gè)Item不是關(guān)于tr1::shared_ptr的——它是關(guān)于“使接口容易被正確使用不容易被誤用”這個(gè)議題的——但是使用tr1::shared_ptr是一個(gè)如此容易的消除客戶錯(cuò)誤的方法,所以值得將使用它的代價(jià)做一個(gè)概述。Tr1::shared_ptr的最一般的實(shí)現(xiàn)來自Boost(Item 55)。Boost中的shared_ptr占用內(nèi)存是原生指針的兩倍,為bookkeeping(引用計(jì)數(shù))和deleter-specific(專屬刪除器) 數(shù)據(jù)分配動態(tài)內(nèi)存,調(diào)用刪除器的時(shí)候使用虛函數(shù),當(dāng)在一個(gè)應(yīng)用中修改引用計(jì)數(shù)時(shí),如果它認(rèn)為自己是多線程的,會引發(fā)線程同步開銷。(你可以通過定義一個(gè)預(yù)處理符號來disable多線程支持)一句話,它比原生指針占用內(nèi)存多,比原生指針慢,并且使用了輔助的動態(tài)內(nèi)存。但是在許多應(yīng)用中,這些額外的運(yùn)行時(shí)開銷是不明顯的,但是客戶錯(cuò)誤的消除對每個(gè)人來說都是顯而易見的。
?
4.總結(jié)
- 好的接口容易被正確使用不容易被誤用,你應(yīng)該使所有的接口滿足這兩個(gè)特征。
- 接口被正確使用的方法包括接口的一致性和同內(nèi)建類型的行為兼容。
- 接口不容易被誤用的方法包括,創(chuàng)建新的類型,對類型上的操作進(jìn)行限制,約束對象值,去除客戶管理資源的責(zé)任。
- Tr1::shared_ptr支持個(gè)性化刪除器。這避免了交叉-DLL問題,可以被用來自動unlock互斥器(Item 14)等等。
轉(zhuǎn)載于:https://www.cnblogs.com/harlanc/p/6431766.html
總結(jié)
以上是生活随笔為你收集整理的读书笔记 effective c++ Item 18 使接口容易被正确使用,不容易被误用的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Eclipse下Maven新建Web项目
- 下一篇: 禁止文字选中 css