《深入理解C++11:C++ 11新特性解析与应用》——3.2 委派构造函数
3.2 委派構造函數
類別:類作者
與繼承構造函數類似的,委派構造函數也是C++11中對C++的構造函數的一項改進,其目的也是為了減少程序員書寫構造函數的時間。通過委派其他構造函數,多構造函數的類編寫將更加容易。
首先我們可以看看代碼清單3-9中構造函數代碼冗余的例子。
在代碼清單3-9中,我們聲明了一個Info的自定義類型。該類型擁有2個成員變量以及3個構造函數。這里的3個構造函數都聲明了初始化列表來初始化成員type和name,并且都調用了相同的函數InitRest。可以看到,除了初始化列表有的不同,而其他的部分,3個構造函數基本上是相似的,因此其代碼存在著很多重復。
讀者可能會想到2.7節中我們對成員初始化的方法,那么我們用該方法來改寫一下這個例子,如代碼清單3-10所示。
在代碼清單3-10中,我們在Info成員變量type和name聲明的時候就地進行了初始化。可以看到,構造函數確實簡單了不少,不過每個構造函數還是需要調用InitRest函數進行初始化。而現實編程中,構造函數中的代碼還會更長,比如可能還需要調用一些基類的構造函數等。那能不能在一些構造函數中連InitRest都不用調用呢?
答案是肯定的,但前提是我們能夠將一個構造函數設定為“基準版本”,比如本例中Info()版本的構造函數,而其他構造函數可以通過委派“基準版本”來進行初始化。按照這個想法,我們可能會如下編寫構造函數:
Info() { InitRest(); } Info(int i) { this->Info(); type = i; } Info(char e) { this->Info(); name = e; }這里我們通過this指針調用我們的“基準版本”的構造函數。不過可惜的是,一般的編譯器都會阻止this->Info()的編譯。原則上,編譯器不允許在構造函數中調用構造函數,即使參數看起來并不相同。
當然,我們還可以開發出一個更具有“黑客精神”的版本:
Info() { InitRest(); } Info(int i) { new (this) Info(); type = i; } Info(char e) { new (this) Info(); name = e; }這里我們使用了placement new來強制在本對象地址(this指針所指地址)上調用類的構造函數。這樣一來,我們可以繞過編譯器的檢查,從而在2個構造函數中調用我們的“基準版本”。這種方法看起來不錯,卻是在已經初始化一部分的對象上再次調用構造函數,因此雖然針對這個簡單的例子在我們的實驗機上該做法是有效的,卻是種危險的做法。
在C++11中,我們可以使用委派構造函數來達到期望的效果。更具體的,C++11中的委派構造函數是在構造函數的初始化列表位置進行構造的、委派的。我們可以看看代碼清單3-11所示的這個例子。
可以看到,在代碼清單3-11中,我們在Info(int)和Info(char)的初始化列表的位置,調用了“基準版本”的構造函數Info()。這里我們為了區分被調用者和調用者,稱在初始化列表中調用“基準版本”的構造函數為委派構造函數(delegating constructor),而被調用的“基準版本”則為目標構造函數(target constructor)。在C++11中,所謂委派構造,就是指委派函數將構造的任務委派給了目標構造函數來完成這樣一種類構造的方式。
當然,在代碼清單3-11中,委派構造函數只能在函數體中為type、name等成員賦初值。這是由于委派構造函數不能有初始化列表造成的。在C++中,構造函數不能同時“委派”和使用初始化列表,所以如果委派構造函數要給變量賦初值,初始化代碼必須放在函數體中。比如:
struct Rule1 {int i;Rule1(int a): i(a) {}Rule1(): Rule1(40), i(1) {} // 無法通過編譯 };Rule1的委派構造函數Rule1()的寫法就是非法的。我們不能在初始化列表中既初始化成員,又委托其他構造函數完成構造。
這樣一來,代碼清單3-11中的代碼的初始化就不那么令人滿意了,因為初始化列表的初始化方式總是先于構造函數完成的(實際在編譯完成時就已經決定了)。這會可能致使程序員犯錯(稍后解釋)。不過我們可以稍微改造一下目標構造函數,使得委派構造函數依然可以在初始化列表中初始化所有成員,如代碼清單3-12所示。
在代碼清單3-12中,我們定義了一個私有的目標構造函數Info(int, char),這個構造函數接受兩個參數,并將參數在初始化列表中初始化。而且由于這個目標構造函數的存在,我們可以不再需要InitRest函數了,而是將其代碼都放入Info(int, char)中。這樣一來,其他委派構造函數就可以委托該目標構造函數來完成構造。
事實上,在使用委派構造函數的時候,我們也建議程序員抽象出最為“通用”的行為做目標構造函數。這樣做一來代碼清晰,二來行為也更加正確。讀者可以比較一下代碼清單3-11和代碼清單3-12中Info的定義,這里我們假設代碼清單3-11、代碼清單3-12中注釋行的“其他初始化”位置的代碼如下:
type += 1;那么調用Info(int)版本的構造函數會得到不同的結果。比如如果做如下一個類型的聲明:
Info f(3);這個聲明對代碼清單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所示。
代碼清單3-13所示就是這樣一種鏈狀委托構造,這里我們使Info()委托Info(int)進行構造,而Info(int)又委托Info(int, char)進行構造。在委托構造的鏈狀關系中,有一點程序員必須注意,就是不能形成委托環(delegation cycle)。比如:
struct Rule2 {int i, c;Rule2(): Rule2(2) {}Rule2(int i): Rule2('c') {}Rule2(char c): Rule2(2) {} };Rule2定義中,Rule2()、Rule2(int)和Rule2(char)都依賴于別的構造函數,形成環委托構造關系。這樣的代碼通常會導致編譯錯誤。
委派構造的一個很實際的應用就是使用構造模板函數產生目標構造函數,如代碼清單3-14所示。
在代碼清單3-14中,我們定義了一個構造函數模板。而通過兩個委派構造函數的委托,構造函數模板會被實例化。T會分別被推導為vector::iterator和deque::iterator兩種類型。這樣一來,我們的TDConstructed類就可以很容易地接受多種容器對其進行初始化。這無疑比羅列不同類型的構造函數方便了很多。可以說,委托構造使得構造函數的泛型編程也成為了一種可能。
此外,在異常處理方面,如果在委派構造函數中使用try的話,那么從目標構造函數中產生的異常,都可以在委派構造函數中被捕捉到。我們可以看看代碼清單3-15所示的例子。
在代碼清單3-15中,我們在目標構造函數DCExcept(int, double)拋出了一個異常,并在委派構造函數DCExcept(int)中進行捕捉。編譯運行該程序,我們在實驗機上獲得以下輸出:
going to throw! caught exception. terminate called after throwing an instance of 'int' Aborted可以看到,由于在目標構造函數中拋出了異常,委派構造函數的函數體部分的代碼并沒有被執行。這樣的設計是合理的,因為如果函數體依賴于目標構造函數構造的結果,那么當目標構造函數構造發生異常的情況下,還是不要執行委派構造函數函數體中的代碼為好。
其實,在Java等一些面向對象的編程語言中,早已經支持了委派構造函數這樣的功能。因此,相比于繼承構造函數,委派構造函數的設計和實現都比較早。而通過成員的初始化、委派構造函數,以及繼承構造函數,C++中的構造函數的書寫將進一步簡化,這對程序員尤其是庫的編寫者來說,無疑是有積極意義的。
總結
以上是生活随笔為你收集整理的《深入理解C++11:C++ 11新特性解析与应用》——3.2 委派构造函数的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Oracle_sql优化基础——优化器总
- 下一篇: [6/N] 论得趣