那么這樣實在是太糟糕了,因為在調(diào)用Sing()和Dancing()時我們甚至不知道調(diào)用的對象的內(nèi)部狀態(tài)是非法的,這也就成為bug的一個源泉.
總結(jié)一下上面的例子中遇到的問題:在以特定的參數(shù)構(gòu)造一個對象時,如果參數(shù)非法或構(gòu)造失敗,我們應(yīng)當(dāng)向調(diào)用者反饋這一信息.
對于一般的成員函數(shù),如果能夠有返回值,那么我們可以通過返回值來標(biāo)識傳遞給函數(shù)的參數(shù)非法或內(nèi)部運行失敗這種情況.但是對于構(gòu)造函數(shù),因為它不能有返回值,所以,我們必須使用其它的方法來向調(diào)用者反饋"傳遞給構(gòu)造函數(shù)的參數(shù)非法或構(gòu)造失敗".
針對于這一問題有三種解決方案:第一種方案:在構(gòu)造函數(shù)的參數(shù)中傳遞一個額外的參數(shù)用于標(biāo)識構(gòu)造是否成功.在這種方案的指導(dǎo)下,代碼如下:
然后我們可以這樣使用:
這種方法是可行的,但是代碼看起來過于丑陋且不夠直觀,這里只是作為一種方案提出并不推薦使用.第二種方案:使用兩段構(gòu)造的形式.所謂的兩段構(gòu)造是指一個對象的內(nèi)部狀態(tài)的初始化分兩步完成,將構(gòu)造函數(shù)中的部分初始化操作轉(zhuǎn)移到一個輔助初始化的成員函數(shù)中:第一步是通過構(gòu)造函數(shù)來完成部分內(nèi)部狀態(tài)的初始化.第二步是通過類似于 Initialize 之類的成員函數(shù)來完成對象內(nèi)部狀態(tài)的最終初始化.
兩段構(gòu)造的形式在 MFC 中廣泛使用.在MFC中我們經(jīng)??吹筋愃朴?Initialize , Create 之類的函數(shù).基于兩段構(gòu)造的形式,代碼如下:
在這種情況下,我們應(yīng)當(dāng)這樣來使用People:
這種方案似乎比第一種方案更優(yōu),但是仍有一個潛在的問題:對象是以兩步構(gòu)造完成的.第一步構(gòu)造是由構(gòu)造函數(shù)來完的,OK,這一點我們不用擔(dān)心,編譯器幫我們保證.但是第二步是由類似于 Initialize 之類的成員函數(shù)來完成的,如果我們在構(gòu)造一個People對象之后忘記了調(diào)用 Initialize ,那么這個對象的內(nèi)部狀態(tài)仍然是非法的,后續(xù)的操作也將由此引發(fā)bug.這也是"兩段構(gòu)造"這種形式受到詬病的原因之一.另一方面,"兩段構(gòu)造"的形式與C++的"RAII",Resource Acquisition Is Initialization(資源獲取即初始化),這一原則相違背.因為以"兩段構(gòu)造"這種形式設(shè)計的class People 在構(gòu)造一個對象時,它的內(nèi)部狀態(tài)實際上并沒有完全初始化,我們需要調(diào)用 Initialize 來輔助完成最終的初始化.所以,盡管"兩段構(gòu)造"這種方案可以解決我們所遇到的"對構(gòu)造函數(shù)參數(shù)非法進行反饋"這個問題,但是這種方案并不夠優(yōu)雅.
但是為什么MFC會先擇"兩段構(gòu)造"這種形式呢,因為在C++發(fā)展的初期,當(dāng)異常機制還不是足夠成熟,沒有得到廣泛的認(rèn)可和使用時,MFC中選擇兩段構(gòu)造或許也是情理之中的,也許還有其它的原因,類似的類庫還有ACE...
當(dāng)然,在某些情況下,使用兩段構(gòu)造也有其獨到的好處.下面所設(shè)計的場景可能有一些牽強,但只是為了力求簡單并能夠說明問題.(代碼進行了大量簡化)
然后在我們的系統(tǒng)中,我們需要使用一個 server pool , 在系統(tǒng)啟動時,我們需要 server pool 中有 100 個 Server 可用.
在系統(tǒng)負(fù)載最大的時候,假定100個Server可以勝任,但是在大多數(shù)情況下,我們只需要少量的Server即可以完成任務(wù).在這種情況下: Server serverPool[ 100 ]; 將會消耗大量的資源(而且大部分資源我們并不會使用),這是我們不愿意接受的.之所以出現(xiàn)這種情況,因為我們在構(gòu)造函數(shù)中分配了大量資源,這種分配是隨構(gòu)造函數(shù)的調(diào)用而自動完成的.
這時,如果我們使用"兩段構(gòu)造"的方法就能在一定的程度上解決這個問題.
在這種情況下: Server serverPool[ 100 ]; 的開銷就很小了,我們可以很好地控制對系統(tǒng)資源的使用,而不會浪費.當(dāng)然,當(dāng)我們從 serverPool 中獲取一個 Server 對象時,我們要調(diào)用 Initialize 進行最終的初始化操作.第三種方案:使用異常即是當(dāng)用于構(gòu)造 People 對象的參數(shù)非法時,我們選擇在構(gòu)造函數(shù)中拋出一個異常來反饋給調(diào)用者"參數(shù)非法,構(gòu)造失敗"的相關(guān)信息.
那么我們可以這樣使用:
這種方案似乎是最優(yōu)的:符合RAII原則,也符合B.S等一批老大推行的"現(xiàn)代C++程序設(shè)計風(fēng)格".
但是很多在開發(fā)一線上的同學(xué)們都反對在代碼中使用異常,實際上我也不愿意在代碼中使用異常,至少不愿意看到類似于java代碼中那樣鋪天蓋地的"throw try catch".我對異常的使用也僅僅是局限在類似于那些適合"用異常來代替兩段構(gòu)造"的場景中,對于其它的情況,我更愿意用返回錯誤碼來標(biāo)識函數(shù)內(nèi)部的運行狀態(tài),而不是通過拋出異常的形式來告知調(diào)用者.
C++規(guī)定:如果執(zhí)行構(gòu)造函數(shù)的過程中產(chǎn)生異常,那么這個未被成功構(gòu)造的對象的析構(gòu)函數(shù)將不會被調(diào)用.這一點在很大程度上為我們在構(gòu)造函數(shù)中拋出異常的安全性提供了C++語言級的保證,當(dāng)然,其它的安全性需要我們自己保證.對于"向調(diào)用者反饋構(gòu)造函數(shù)參數(shù)非法或構(gòu)造失敗的相關(guān)信息"這個問題,"基于異常"和"基于兩段構(gòu)造"這兩種方案我都使用過一段時間,目的是確定對于自己而言到底哪一種方案用起來更舒服更適合自己.最終的結(jié)果是我選擇了"基于異常"這種形式.
對于"基于異常"和"基于兩段構(gòu)造",沒有哪一種能在所有的情況下都是最優(yōu)的解決方案,印證了那句名言"there is no silver bullet".如何在不同的場景中選擇其一作為最優(yōu)的解決方案是我們在設(shè)計時需要權(quán)衡的問題.
個人愚見,錯漏之處還請指正,歡迎大家踴躍發(fā)言:)