天天看點

《深入了解C++11:C++ 11新特性解析與應用》——3.2 委派構造函數

類别:類作者

與繼承構造函數類似的,委派構造函數也是c++11中對c++的構造函數的一項改進,其目的也是為了減少程式員書寫構造函數的時間。通過委派其他構造函數,多構造函數的類編寫将更加容易。

首先我們可以看看代碼清單3-9中構造函數代碼備援的例子。

《深入了解C++11:C++ 11新特性解析與應用》——3.2 委派構造函數

在代碼清單3-9中,我們聲明了一個info的自定義類型。該類型擁有2個成員變量以及3個構造函數。這裡的3個構造函數都聲明了初始化清單來初始化成員type和name,并且都調用了相同的函數initrest。可以看到,除了初始化清單有的不同,而其他的部分,3個構造函數基本上是相似的,是以其代碼存在着很多重複。

讀者可能會想到2.7節中我們對成員初始化的方法,那麼我們用該方法來改寫一下這個例子,如代碼清單3-10所示。

《深入了解C++11:C++ 11新特性解析與應用》——3.2 委派構造函數

在代碼清單3-10中,我們在info成員變量type和name聲明的時候就地進行了初始化。可以看到,構造函數确實簡單了不少,不過每個構造函數還是需要調用initrest函數進行初始化。而現實程式設計中,構造函數中的代碼還會更長,比如可能還需要調用一些基類的構造函數等。那能不能在一些構造函數中連initrest都不用調用呢?

答案是肯定的,但前提是我們能夠将一個構造函數設定為“基準版本”,比如本例中info()版本的構造函數,而其他構造函數可以通過委派“基準版本”來進行初始化。按照這個想法,我們可能會如下編寫構造函數:

這裡我們通過this指針調用我們的“基準版本”的構造函數。不過可惜的是,一般的編譯器都會阻止this->info()的編譯。原則上,編譯器不允許在構造函數中調用構造函數,即使參數看起來并不相同。

當然,我們還可以開發出一個更具有“黑客精神”的版本:

這裡我們使用了placement new來強制在本對象位址(this指針所指位址)上調用類的構造函數。這樣一來,我們可以繞過編譯器的檢查,進而在2個構造函數中調用我們的“基準版本”。這種方法看起來不錯,卻是在已經初始化一部分的對象上再次調用構造函數,是以雖然針對這個簡單的例子在我們的實驗機上該做法是有效的,卻是種危險的做法。

在c++11中,我們可以使用委派構造函數來達到期望的效果。更具體的,c++11中的委派構造函數是在構造函數的初始化清單位置進行構造的、委派的。我們可以看看代碼清單3-11所示的這個例子。

《深入了解C++11:C++ 11新特性解析與應用》——3.2 委派構造函數

可以看到,在代碼清單3-11中,我們在info(int)和info(char)的初始化清單的位置,調用了“基準版本”的構造函數info()。這裡我們為了區分被調用者和調用者,稱在初始化清單中調用“基準版本”的構造函數為委派構造函數(delegating constructor),而被調用的“基準版本”則為目标構造函數(target constructor)。在c++11中,所謂委派構造,就是指委派函數将構造的任務委派給了目标構造函數來完成這樣一種類構造的方式。

當然,在代碼清單3-11中,委派構造函數隻能在函數體中為type、name等成員賦初值。這是由于委派構造函數不能有初始化清單造成的。在c++中,構造函數不能同時“委派”和使用初始化清單,是以如果委派構造函數要給變量賦初值,初始化代碼必須放在函數體中。比如:

rule1的委派構造函數rule1()的寫法就是非法的。我們不能在初始化清單中既初始化成員,又委托其他構造函數完成構造。

這樣一來,代碼清單3-11中的代碼的初始化就不那麼令人滿意了,因為初始化清單的初始化方式總是先于構造函數完成的(實際在編譯完成時就已經決定了)。這會可能緻使程式員犯錯(稍後解釋)。不過我們可以稍微改造一下目标構造函數,使得委派構造函數依然可以在初始化清單中初始化所有成員,如代碼清單3-12所示。

《深入了解C++11:C++ 11新特性解析與應用》——3.2 委派構造函數

在代碼清單3-12中,我們定義了一個私有的目标構造函數info(int, char),這個構造函數接受兩個參數,并将參數在初始化清單中初始化。而且由于這個目标構造函數的存在,我們可以不再需要initrest函數了,而是将其代碼都放入info(int, char)中。這樣一來,其他委派構造函數就可以委托該目标構造函數來完成構造。

事實上,在使用委派構造函數的時候,我們也建議程式員抽象出最為“通用”的行為做目标構造函數。這樣做一來代碼清晰,二來行為也更加正确。讀者可以比較一下代碼清單3-11和代碼清單3-12中info的定義,這裡我們假設代碼清單3-11、代碼清單3-12中注釋行的“其他初始化”位置的代碼如下:

那麼調用info(int)版本的構造函數會得到不同的結果。比如如果做如下一個類型的聲明:

這個聲明對代碼清單3-11中的info定義而言,會導緻成員f.type的值為3,(因為info(int)委托info()初始化,後者調用initrest将使得type的值為4。不過info(int)函數體内又将type重寫為3)。而依照代碼清單3-12中的info定義,f.type的值将最終為4。從代碼編寫者角度看,代碼清單3-12中info的行為會更加正确。這是由于在c++11中,目标構造函數的執行總是先于委派構造函數而造成的。是以避免目标構造函數和委托構造函數體中初始化同樣的成員通常是必要的,否則則可能發生代碼清單3-11錯誤。

而在構造函數比較多的時候,我們可能會擁有不止一個委派構造函數,而一些目标構造函數很可能也是委派構造函數,這樣一來,我們就可能在委派構造函數中形成鍊狀的委派構造關系,如代碼清單3-13所示。

《深入了解C++11:C++ 11新特性解析與應用》——3.2 委派構造函數

代碼清單3-13所示就是這樣一種鍊狀委托構造,這裡我們使info()委托info(int)進行構造,而info(int)又委托info(int, char)進行構造。在委托構造的鍊狀關系中,有一點程式員必須注意,就是不能形成委托環(delegation cycle)。比如:

rule2定義中,rule2()、rule2(int)和rule2(char)都依賴于别的構造函數,形成環委托構造關系。這樣的代碼通常會導緻編譯錯誤。

委派構造的一個很實際的應用就是使用構造模闆函數産生目标構造函數,如代碼清單3-14所示。

《深入了解C++11:C++ 11新特性解析與應用》——3.2 委派構造函數
《深入了解C++11:C++ 11新特性解析與應用》——3.2 委派構造函數

在代碼清單3-14中,我們定義了一個構造函數模闆。而通過兩個委派構造函數的委托,構造函數模闆會被執行個體化。t會分别被推導為vector::iterator和deque::iterator兩種類型。這樣一來,我們的tdconstructed類就可以很容易地接受多種容器對其進行初始化。這無疑比羅列不同類型的構造函數友善了很多。可以說,委托構造使得構造函數的泛型程式設計也成為了一種可能。

此外,在異常處理方面,如果在委派構造函數中使用try的話,那麼從目标構造函數中産生的異常,都可以在委派構造函數中被捕捉到。我們可以看看代碼清單3-15所示的例子。

《深入了解C++11:C++ 11新特性解析與應用》——3.2 委派構造函數

在代碼清單3-15中,我們在目标構造函數dcexcept(int, double)抛出了一個異常,并在委派構造函數dcexcept(int)中進行捕捉。編譯運作該程式,我們在實驗機上獲得以下輸出:

可以看到,由于在目标構造函數中抛出了異常,委派構造函數的函數體部分的代碼并沒有被執行。這樣的設計是合理的,因為如果函數體依賴于目标構造函數構造的結果,那麼當目标構造函數構造發生異常的情況下,還是不要執行委派構造函數函數體中的代碼為好。

其實,在java等一些面向對象的程式設計語言中,早已經支援了委派構造函數這樣的功能。是以,相比于繼承構造函數,委派構造函數的設計和實作都比較早。而通過成員的初始化、委派構造函數,以及繼承構造函數,c++中的構造函數的書寫将進一步簡化,這對程式員尤其是庫的編寫者來說,無疑是有積極意義的。

繼續閱讀