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