兩段構造也是聲名狼藉得很,比之於MFC,好不了多少,貌似MFC中到處都是兩段構造,難道兩段構造的聲譽也是受MFC所累。定義完了一個對象變量之後,還要再調用一次該對象的Create函數,而且還要Create成功了之後,才能對該對象做進一步的操作,否則對象將一直處於非法狀態。這種代碼方式寫起來確實很惡心,為何不直接在構造函數中直接Create,不成功就拋出異常,然後對象就流產了,好過它半死不活地一直苟延殘喘於世上,累己累人。其實,MFC選擇兩段構造也是有苦衷:1、先是很久很久以前,VC編譯器對異常的支持不怎麼好,當然,現在的VC編譯器,自然今時不比往日,但是,還要兼容以往的代碼;2、然後是MFC的設計,它只是對API做了一層薄薄的包裝,薄薄的意思,就是,不管怎麼搗鼓,都難以將WINDOWS系統中的各種對象包裝成一個干淨的C++對象了,因為,API本身就采用兩段構造。可不是嗎?定義一個句柄變量,然後CreateXXX返回結果,返回值非法,表示創建失敗。失敗了,還要霸王硬上弓,後果會怎麼樣,這誰也不知道。
理論上,構造函數拋出異常確實很優雅,代碼也更具美感,並且,其行為也更加明確,要麼就處理,要麼,就等著程序異常退出。但是,實際上,異常這種東西,真正實現執行起來,卻相當的困難。更何況,如果完全丟棄兩段法,除了異常,還會引入一些新的問題,正所謂:“前門驅虎,後門進狼”,進來不只是一只狼,而是好幾只。生活的奧妙,就在於制造出新的問題,以解決舊的問題。
構造函數中直接調用Create,就表示了用戶一定義一個類型變量,程序就會馬上啟動Create函數,也就意味著可能將創建窗口對象、內核對象、甚至啟動新的線程等等,這些操作都不是省油的燈,構造函數中做了太多事情,會有隱藏太多細節之嫌,代碼本來就是為了隱藏細節,這個多事之罪名暫且不論;但是,用戶沒法對創建過程Say NOT,也即是說,用戶一定義對象變量,就只能接受它的高昂的創建過程。難道,一開始就讓對象進入有效狀態,這都有錯嗎?確實是的。有時候,用戶只是先想聲明(定義)對象,等必要(時機成熟)的時候,再讓它進入有效狀態。咦,用戶這樣寫代碼,不太好吧,應該強制他/她等到了那個時候,再定義對象變量。變量怎麼可以隨隨便便就定義呢?應該在要使用的時候,才定義它,這才是良好的代碼風格。但是,有些情況,確實需要先暫時定義非法狀態下的對象變量,比如,這個對象是另一個對象(擁有者)的成員變量時,那也沒什麼,強制用戶在必要的時候,才定義擁有者對象變量。但是,假如這個擁有者必須是全局變量,那該怎麼辦?那也沒什麼,將擁有者定義為指針變量就是了?好了,本來只是要對象創建失敗的情況,現在還要考慮內存分配的細節,然後接著就是new delete,然後就是各種智能指針閃亮登台演出,更糟糕的是,對象有效無效的問題依然沒有根除,因為,只要引入指針,每次使用指針,就必須檢查指針是否有效,咦,難道操作空指針不會拋出異常嗎?C++規范中,操作空指針屬後果未確定的行為,對C++而言,未確定往往就是最糟糕的意思。此外,鑒於對象只能一直處於有效狀態,它就不可能提供讓對象進入無效狀態的操作。如果想要讓對象無效,唯一的辦法,就是讓它死去,強制對象啟動析構函數,方法是離開作用域強者delete它。下次要使用它的時候,就再new一次或者定義一次,不,它已經是另外一條新生命了。但是,對於兩段構造的對象,只須Destroy又或者Create,對象可以永遠只有一個。此外,二段構造頗具擴展性,很輕易地就可搞成三段構造,每一步,用戶都有選擇的權利。但構造異常就沒有這個優點。
考慮到構造函數中的參數問題,比如,月份的參數,大家都知道,有效值只在1-12月之間。不討論這種情況下,非法的參數傳遞是否屬於代碼的邏輯問題。對此,構造異常指導下的對象是不可能出現無參(沒有參數或者參數都有缺省值)的構造函數,因此,它們也都不能用於數組,難以應用於全局變量、靜態變量、作為其他對象的數據成員,如果非要在這些場合下使用它們,比如占位符的作用,唯有用上指針,於是伴隨而來的,又如上文所述,使用指針之前,必須檢查指針的有效性,只怕不會比檢查二段構造的有效性好多少。
二段構造不輕易剝奪用戶的權利,提供更多選擇,可用於數組、堆棧、STL中的容器,要它死,它就死,要它活,它就活,但是,它可以從來都未曾消失過,要做的,僅僅是在使用它時,清楚它是死是活就行了,不過多加幾次判斷而已。相比之下,構造異常就更具侵入性了,一旦用上,就只能被迫遵照它的規則行事。
其實,兩段構造與構造異常,都很惡心,只要一處代碼中用到了它,所有與之相關的代碼都沒法脫身。差別不過在於誰比誰惡心而已,這個,視各人的口味而不同。對於本人這種害怕分配內存,釋放內存,更加畏懼異常的人來說(這並不表示本人寫不出異常安全的代碼),當然優先選擇二段構造,MORE EFFECTIVE的條款中,聲稱,如無必要,不要提供缺省的構造函數,以免對象陷入半死不活的狀態中。而我的習慣作法則是,如無必要,必須提供缺省的構造函數,不要輕易剝奪用戶想要使用對象數組的權利,或者是由於不提供缺省的構造函數,而由此引起的種種不便。
好了,既然程序中決定用二段構造了,那麼,假如用戶定義了一個對象,忘了再構造一次,但是又要執行其他操作,怎麼辦?嗯,那也沒什麼,既然用戶不遵守契約,我們的對象自然可以做出種種不確定的行為。當然,別忘了,在其他的每一個操作上都添加幾條assert語句,盡管這很惡心,也聊勝於無,減少點罪惡感,以便於在調試版中找出問題
作者 huaxiazhihuo