========================
Effective C++ 設計與聲明
書作者:Scott Meyers
原筆記作者:Justin
========================
Item 18 : 接口應該容易被正確使用,不易被誤用
--------------------------------------------------
tag:消除客戶的資源管理責任 tr1::shared_ptr cross-DLL problem
用戶使用接口時卻沒有獲得預期的行為,這個代碼不應該通過編譯。
用錯的可能有:
·調用接口時輸入了錯誤的參數。
如(一個接受年、月、日為參數的接口函數,用戶可以輕易給出各種錯誤的輸入),
解決辦法:用對象來約束參數輸入的范圍(不接受簡單的整數作為輸入,而是Date、Mon、Year對象)
struct Day{ explicit Day(int d):val(d){} int val;}
class Month{
public:
static Month May(){ return Month(5); }
private:
explicit Month(int m);
}
Date d(Month::Mar(),Day(20),Year(1995));
·用常規的用法調用“特別”設計的接口。所以需要盡可能的把自己的設計往常規上靠:數據對象的行為要盡可能符合內建對象(比如int)的行為;接口的名字和意義要盡可能一致(比如STL中的容器基本都有一個叫做size的返回容器大小的接口)……這樣做鼓勵用戶去正確的看待和使用你的接口。
·忘了處理調用接口后的遺留問題。因此不要讓用戶去“記得”做一些事情。
如設計一個接口返回一個指向某新建對象的指針,該接口的用戶需要“記得”去釋放這個指針所指的對象:如果用戶忘了釋放或釋放了好幾次,后果就是@#¥%
解決的辦法之一是讓該接口返回一個智能指針(嗯……印象模糊了?去看Item14),這樣用戶用完了就可以“忘記”這個指針:它自己會處理后事。
·所謂的“跨DLL問題”(cross DLL problem):在一個DLL中new一個對象,然后對象被傳到另外一個DLL里被delete。大師推薦用shared_ptr因為它解決了這個問題。
代價:額外對象的創建和銷毀需要時間空間。比如boost的shared_ptr就是普通指針的兩倍大小,還有額外的對象操作時間+過程動態內存分配等。
實際上有些底層代碼根本沒這個資本提供這樣的“豪華裝備”,不過有這樣的思想還是很重要D……
Item 19 :設計class猶如設計type
----------------------------------------
tag:class design
·小心設計類的創建和銷毀方式。比如說Item8和Item16
·認真考慮如何區分類的構造函數和賦值(assignment)操作符。即初始化與賦值的差別。
·注意實現類的傳值(passed by value)。這個實際上是在說要注意拷貝構造函數的實現。
·切勿忽略類對非法輸入的處理。其實是要注意各種出錯情況,是否需要拋出異常以及如何實現異常處理。
·需要審視類所在的繼承體系。
如果該類有父類,那么必定要受到父類的一些限制,特別是函數是否為虛構;如果該類有子類,那么就要考慮是不是一些函數需要定義為虛函數,比如說析構函數。
·謹慎實現類對象與其他類型對象的轉換。這一點稍有些復雜:如果有將T1轉換為T2的需求,就有隱式轉換和顯式轉換兩種方式。
對于前者,可以編寫一個(隱式的)轉換函數(參考Item15里面的隱式轉換咯~),或者是通過額外編寫一個T2的構造函數來實現T1向T2的轉換。
對于后者,Scott說寫一個(顯式的)轉換函數就可以了。(同樣,在Item15里也有顯式轉換函數的例子)
·需要考慮該類需要參與哪些運算。很明顯,如果需要參與A運算就要相應定義類的A運算符函數。大師在這里提的另外一點是,這些運算符號函數有些應該是成員函數,有些不應該。原因在Item23、24、26【555我還沒看到,留空】
·不要提供不應該暴露的標準函數。這里的標準函數指的是構造/析構/拷貝等等可能由編譯器“自愿”為你生成的函數,如果不希望它們中的一些被外界調用,就聲明為私有(private)。沒印象了?降級到Item6重新學習~
·注意設計類成員的訪問權限。公有(public)、保護(protected)、私有(private)應該用哪一種?有沒有需要定義友元?或者是干脆來一個類中類?都需要考慮。
·認真審查類的隱性限制。性能上的要求、使用資源的限制或是出錯時的處理都有可能影響到類的具體設計和實現。
·謹慎考慮類的適用范圍。也就是說如果某個設計會可能用在很多方面,適用于許多不同的實際對象。也許這個時候你需要設計的不是一個類,而是一個類模板。
最后一點其實應該放在第一位:你真的需要定義一個類嗎?如果僅僅是在繼承某類的基礎上加一兩個成員,是不是非成員函數或模板就已經夠了捏?
第19招其實更像是個check list,在準備動手設計之前,一一比對打勾劃叉,應該可以提前避免很多人間慘劇……
Item 20 : 用 傳const引用 替換 傳值
-------------------------------------------
tag: const引用 值傳遞
C++傳遞對象的時候默認是傳值的(pass-by-value),而這樣的傳遞自然是昂貴的:這當中包含了臨時對象的構造/析構,以及臨時對象中的對象的構造/析構,運氣背點還可能有對象中的對象中的對象的構造/析構……(有好的不學,去學C@#¥%)
·相對于傳“值”,一個更好的替代方法是傳“const引用”(pass-by-reference-to-const)。
·傳值與傳指針的一個區別是,通過傳值傳遞的對象并不是原來的對象,而是一個復制品,所以隨便你打它罵它,真身都不會受到影響。
·而通過傳指針的對象和原來的對象就是同一家伙,改動一個另外一個也受到相同的影響。而這有時候并不是我們想要的結果。
·考慮到傳值代價太高,傳“const引用”就成了一個很好的替代品。
·傳“const引用”的另外一個好處在于避免了“剝皮問題”(slicing problem,侯捷大師的版本是“對象切割問題”,我用這個中文名字是為了更容易記住:))
用傳值方式傳參的函數,如果某參數的類型是一個父類對象,而實際傳遞的參數是一個子類對象,只有該對象的父類部分會被構造并傳遞到函數中,子類部分的成員,作為父類對象的“皮”,就被血淋淋的剝掉了……
而如果用傳“const引用”方式,就沒有這種慘無人道的狀況:本來父類的指針就可以用來指向一個子類對象,天經地義。
例外:對于內置類型(bulit-in type)對象以及STL中的迭代器、函數對象,Scott還是建議使用傳值方式傳遞,原因是他們本來就是被設計成適合傳值傳遞的。
(個人觀點:大師說:“……it's not unreasonable to choose pass-by-value。”,注意這里有句潛臺詞:其實對以上類型用傳“const引用”方式傳遞也是可以的。)
如果你認為上面兩種情況可以用傳值傳遞是因為它們,比如說內置類型對象,的大小本來就小,進而得出小數據類型就可以用傳值傳遞,就打錯特錯了。小對象的構造/析構過程完全可能很恐怖。
再退一步,哪怕某個類型很小,它的構造/析構函數也簡單到可以忽略不計,我們還是不能以此斷定可以用傳值傳遞這種類型的對象:因為編譯器往往會做出一些蠢事。書中的一個例子是,對于一些編譯器可以接受把一個double類型對象存入寄存器,但是如果你給它一個只有一個double成員的對象交給它,它卻拒絕將該對象存入寄存器。(什么事讓編譯器插一手,不是問題也有了問題……)
最后還有個理由,雖然某對象現在很小,可是隨著社會的發展人類的進步,有可能兩年后它就會變成一個龐然大物,到時候用傳值也會變得不合適。
·除了內置類型和STL的迭代器、函數對象外,其他的對象傳遞時,用傳“const引用”代替傳值吧。
Item 21: 該換回對象時別返回它的reference
---------------------------------------------------
tag:
如果一個函數可能返回一個對原來不存在的對象的引用,那么函數就要自己去創建這個對象:要么在棧上(stack)要么在堆上(heap)。
第一種情況中,函數中定義了局部對象,然后返回對該對象的引用。對象在函數結束后自動銷毀,引用指向無效的地址。
第二種情況,函數使用new動態創建了一個對象,然后返回對該對象的引用。粗看沒有問題,因為這個返回的引用還是有效的。
但是細想就會發現:我們能確保這個對象被正確的收回(delete)嗎?
書中舉了一個很好的例子:一個*運算符函數,接受兩個乘法運算數,返回一個積。
如果在函數中動態創建一個對象來存儲乘積,并返回對這個新對象的引用,那么下面的計算就會帶來內存泄漏:
Y=A*B*C
因為在這個“連續”乘法中,有兩個“乘積對象”被創建,但是我們丟失了第一次乘法創建的對象的指針。
所以這樣的做法是不妥的。
也許大師有被問過:那么對于第一種情況我們可不可以返回一個靜態(static)對象的引用?書中用了同樣的例子來回答:NO。
if (A*B == C*D) {//..}
如果返回靜態對象的引用,上面的判斷語句永遠得到true值,因為“==”號兩邊的運算結果是指向同一塊數據的引用。
不知道是不是后面又有刨根問題的學生追問:那我能不能用一個靜態對象的數組來存放不同此運算的結果?大師懶得舉例子了,我猜想也沒必要:這樣的方案帶來的副作用及其開銷本身就已經大于原來要解決的問題了吧?
不要嘗試在函數中返回對局部對象(存儲于棧)的引用,也不要對動態創建的對象(存儲于堆)做同樣的蠢事,而如果真有打算、非常渴望、十分想要返回對靜態對象的引用,在這么做之前也要考慮清楚會不會有上面例子中的情況出現。
至少,返回對象不會有上面列出的種種錯誤危險,僅僅是有可能帶來創建額外對象的開銷而已,而這個開銷的可能還有可能被排除,如果你用的編譯器足夠聰明的話。
Item 22: 將成員變量聲明為private
-----------------------------------------------------
如果數據成員都是私有的,那么訪問這些成員就只能通過函數進行。于是用戶就不需要費心考慮到底要用什么方式去訪問數據成員:因為只有定義了的函數可以用。
通過定義數據成員為私有,可以實現函數來設計、約束或禁止對這些成員的各種訪問(讀/寫等)。而如果將其設為公有(public),你將無法得知你的成員會被誰改動,也不知道會是怎樣的改動。
而更重要的好處是封裝(encapsulation):可以方便的通過修改函數來改變成員的訪問方式;在成員被訪問時通知其他對象;實現多線程中的同步等等。
封裝的好處究其本質,是通過對用戶隱藏數據成員來保證類行為的一致性(class invariant)。因為接口被成員訪問函數限制了,類的作者也為自己日后修改類的實現留了后路:如果所有的成員都是公有的,對任何代碼的修改都有可能影響到外界的使用。(因此Scott說“Public means unencapsulated, and practically speaking, unencapsulated means unchangeable, especially for classes that are widely used.”)
那么可不可以聲明為保護(protected)呢?其實道理和前面的公有是一樣的。公有的成員對類的外部完全開放,而保護的成員對類的繼承者完全開放。這個就像兩個區間:(-infinity, +infinity) 和 (0, +infinity),兩者的大小是一樣的。
從分裝的角度,只有兩種訪問級別:私有,及其他。
Item 23: 用 non-member、non-friend 替換 member 函數
------------------------------------------------------
從面向對象的角度來看,非成員函數更有利于數據的封裝。
一個數據成員被越少的代碼訪問到,該成員的封裝程度就越高。越少函數可以直接訪問一個數據成員,該成員的封裝程度就越高。
類的數據成員應該定義為私有。如果這個前提成立,那么能夠訪問一個數據成員的函數便只能是該類的成員函數,或是友元函數。
于是為了更好的封裝數據,在可以完成相同功能的前提下,應該優先考慮使用非成員并且非友元函數。
這里的“非成員并且非友元函數”是針對數據成員所在的類而言的,也就是說這個函數完全可以是其他類的成員,只要是不能直接訪問那個數據成員就可以。
從靈活性上來說,非成員函數更少編譯依賴(compilation dependency),也就更利于類的擴展。
例:一個類可能有多個成員函數,可能有一個函數需要A.h,另外一個函數要包含B.h,那么在編譯這個類時就需要同時包含A.h和B.h,也就是說該類同時依賴兩個頭文件。
如果使用非成員函數,這個時候就可以把這些依賴關系不同的函數分別寫在不同的頭文件中,有可能這個類在編譯時就不需要再依賴A.h或是B.h了。
把這些非成員函數分散定義在不同頭文件中的同時,需要用namespace關鍵字把它們和需要訪問的類放在一起。
// code in class_a.h
namespace AllAboutClassA {
class ClassA { // ..};
// ..
}
// code in utility_1.h
// ..
namespace AllAboutClassA {
void WrapperFunction_1() { // ..};
// ..
}
// ..
// code in utility_2.h
// ..
namespace AllAboutClassA {
void WrapperFunction_2() { // ..};
// ..
}
// ..
這樣一來,雖然這些非成員和類不“住在”一個頭文件里,它們的“心”還是在一起的(在同一個名字空間, namespace, 中)。
如果有需要添加新的非成員函數,我們要做的只是在相同的名字空間中定義這些函數就可以,那個類絲毫不會被影響,也即所謂的易擴展性吧。
對于類的用戶來說,這樣的實現方式(指用非成員函數)就更加合理:
因為作為類的用戶,需要擴展類的時候又不能去修改別人的類(版權?安全性?或者根本就沒有源碼?),就算是通過繼承該類的方式也不能訪問父類的私有數據。
Item 24: 若所有參數皆需類型轉換,為此函數采用 non-member 函數
------------------------------------------------------
tag: operator
若需要為某個函數的所有參數(包括被this指針所指的那個隱喻參數)進行類型轉換,這個函數應該設為 non-member.
---------------------
class Rational{
public:
Rational(int num = 0, int denominator = 1); //構造函數刻意不為 explicit; 允許 int-to-Rational 隱式轉換
...
const Rational operator* (const Rational& rhs) const;
}
Rational oneHalf(1, 2);
result = oneHalf * 2; //right → result = oneHalf.operator*(2);
result = 2 * oneHalf; //wrong! → result = 2.operator*(oneHalf); → result = operator*(2, oneHalf);
---------------------
const Rational operator*(const Rational& lhs, const Rational& rhs){..} //non-member function.
若函數不該成為member,不一定要成為friend.
Item 25: 考慮寫出一個不拋異常的 swap 函數
------------------------------------------------------
tag: 異常處理
·當 std::swap 對你的類型效率不高時,提供一個 swap 成員函數,并確定函數不拋出異常。
·如果你提供一個 member swap,也該提供一個 non-member swap 用來調用前者,
對于 classes(非 templates),請特化 std::swap;
·調用 swap 時應針對 std::swap 使用using 聲明式,然后調用 swap 并不帶任何“命名空間資格修飾”。
·為“用戶定義類型”進行std templates 全特化是好的,但千萬不要嘗試在 std 內加入某些對 std 而言全新的東西。
std::swap的缺省實現:用了一個中間臨時對象,然后兩兩交換。
缺省的方法很簡單,而在一些情況下卻也很耗資源:比如需要交換的是一個很復雜龐大的對象時,創建/拷貝大量數據會使得這種swap的效率顯得非常低下。
--------------------------
更加適應的實現思路:
在類/模板類(class/class template)中定義一個公有的swap成員函數,這個函數負責實現真正的交換操作。同時,這個函數不能拋出異常。
用成員是因為交換操作中可能會需要訪問/交換類的私有成員;用公有(public)來限制是為了給下面的輔助函數(wrapper function)提供接口。
至于不能拋出異常,有兩個原因:
一是Item29中所提到的異常安全性(exception-safety)需要不拋出異常的swap來提供保障(更多細節就到拜讀29條的時候再記錄吧。)
二是一般而言高效的swap函數幾乎都是對內置類型的交換,而對內置類型的操作是不會拋出異常的。
1. 如果需要使用swap的是一個類(而不是模板類),就為這個類全特化std::swap,然后在這個特化版本中調用第一步中實現的swap函數。
class AClass{
public :
void swap(AClass & theOtherA){
using std::swap; // 這一句會在稍后的第3點提到
// 通過調用swap來完成該類的特有交換動作
}
// ..
}
namespace std{
// 在std名字域內定義一個全特化的swap
template <> // 這樣的定義說明是全特化
void swap < AClass > ( AClass & a, AClass & b){
a.swap(b);
}
}
如此一來,用戶可以直接應用同樣的swap函數進行交換操作,而當交換對象是需要特殊對待的AClass對象時,也可以無差別的使用并得到預期的交換結果。
2. 如果我們需要交換的是模板類,那么就不能用全特化std::swap的方法了,偏特化的std::swap也行不通,因為:
C++中不允許對函數進行偏特化(只能對類偏特化),也 就是說不能寫出下面的程序:
namespace std{
// illegal code as C++ doesn't allow partial specialization for function templates
template<typename T>
void swap< AClassTemplate<T> >(AClassTemplate<T>& a, AClassTemplate<T>& b)
{
a.swap(b);
}
}
std名字空間中的內容都是C++標準委員會的老大們定義的,為了保證std內部代碼的正常運作,不允許往里頭添加任何新的模板、類、方程,重載也不可以。
雖然可以像1.那樣寫出全特化的模板函數,但是企圖在std的名字空間添加以下重載的swap(這種重載變相實現了函數的偏特化)(雖然你可以通過編譯,但是會埋下隱患):
namespace std{
template <typename T>
void swap (AClass<T>& a, AClass<T>& b)
{ a.swap(b);}
}
給自己的一個小提醒:因為函數名swap后沒有<>,所以不是偏特化,而是對
namespace std{
template<class _Ty> inline
void swap(_Ty& _X, _Ty& _Y)
{/*..*/}
}
的重載而已。
基于上面兩個原因,一個變通的方法是在該模板類所在的名字空間中編寫一個 非成員的函數模板來調用這個公有的接口:
namespace AClassSpace{
template <typename T>
void swap (AClass<T>& a, AClass<T>& b)
{ a.swap(b);}
}
在限定的名字空間中實現函數,是為了避免“污染”全局的名字空間。而且,不同的名字空間都可以使用一樣的函數名字而不會有沖突。
基于前面第23課所學,使用非成員函數也是應該的了。
至于為什么要函數模板,那就匪常D簡單:因為要交換的是模板@#¥%
pimpl也即pointer to implementation,當需要交換兩個復雜且臃腫的對象時,可以先用兩個指針分別指向著兩個對象
之后對這些對象的操作,包括交換,就只需要通過這兩個指針來進行(交換兩個指針的值便實現了對象的交換)。
posted on 2010-03-15 22:50
Euan 閱讀(529)
評論(0) 編輯 收藏 引用 所屬分類:
C/C++