1. 程式人生 > >More effective C++ 條款25 將建構函式和非成員函式虛擬化

More effective C++ 條款25 將建構函式和非成員函式虛擬化

7.1 Item M25:將建構函式和非成員函式虛擬化
從字面來看,談論“虛擬建構函式”沒有意義。當你有一個指標或引用,但是不知道其指向物件的真實型別是什麼時,你可以呼叫虛擬函式來完成特定型別(type-specific)物件的行為。僅當你還沒擁有一個物件但是你又確切地知道想要的物件的型別時,你才會呼叫建構函式。那麼虛擬建構函式又從何談起呢?

很簡單。儘管虛擬建構函式看起來好像沒有意義,其實它們有非常大的用處(如果你認為沒有意義的想法就沒有用處,那麼你怎麼解釋現代物理學的成就呢?)(因為現代物理學的主要成就是狹義、廣義相對論,量子力學,這些理論看起來都好象很荒謬,不好理解。譯者注)。例如,假設你編寫一個程式,用來進行新聞報道的工作,每一條新聞報道都由文字或圖片組成。你可以這樣管理它們:

class NLComponent { //用於 newsletter components
public: // 的抽象基類
... //包含至少一個純虛擬函式
};
class TextBlock: public NLComponent {
public:
... // 不包含純虛擬函式
};
class Graphic: public NLComponent {
public:
... // 不包含純虛擬函式
};
class NewsLetter { // 一個 newsletter 物件
public: // 由NLComponent 物件
... // 的連結串列組成
private:
list<NLComponent*> components;
};
類之間的關係圖


在NewsLetter中使用的list類是一個標準模板類(STL),STL是標準C++類庫的一部分(參見Effective C++條款49和條款M35)。list型別物件的行為特性有些象雙向連結串列,儘管它沒有以這種方法來實現。物件NewLetter不執行時就會儲存在磁碟上。為了能夠通過位於磁碟的替代物來建立Newsletter物件,讓NewLetter的建構函式帶有istream引數是一種很方便的方法。當建構函式需要一些核心的資料結構時,它就從流中讀取資訊:
class NewsLetter {
public:
NewsLetter(istream& str);
...
};
此建構函式的虛擬碼是這樣的:
NewsLetter::NewsLetter(istream& str)
{
while (str) {
從str讀取下一個component物件;
把物件加入到newsletter的 components物件的連結串列中去;
}
}
或者,把這種技巧用於另一個獨立出來的函式叫做readComponent,如下所示:

class NewsLetter {
public:
...
private:
// 為建立下一個NLComponent物件從str讀取資料,
// 建立component 並返回一個指標。
static NLComponent * readComponent(istream& str);
...
};
NewsLetter::NewsLetter(istream& str)
{
while (str) {
// 把readComponent返回的指標新增到components連結串列的最後,
// "push_back" 一個連結串列的成員函式,用來在連結串列最後進行插入操作。
components.push_back(readComponent(str));
}
}
考慮一下readComponent所做的工作。它根據所讀取的資料建立了一個新物件,或是TextBlock或是Graphic。因為它能建立新物件,它的行為與建構函式相似,而且因為它能建立不同型別的物件,我們稱它為虛擬建構函式。虛擬建構函式是指能夠根據輸入給它的資料的不同而建立不同型別的物件。虛擬建構函式在很多場合下都有用處,從磁碟(或者通過網路連線,或者從磁帶機上)讀取物件資訊只是其中的一個應用。(WQ加註:readComponent()的實現可詳見《湯姆.斯旺C++程式設計祕訣》)

還有一種特殊種類的虛擬建構函式――虛擬拷貝建構函式――也有著廣泛的用途。虛擬拷貝建構函式能返回一個指標,指向呼叫該函式的物件的新拷貝。因為這種行為特性,虛擬拷貝建構函式的名字一般都是copySelf,cloneSelf或者是象下面這樣就叫做clone。很少會有函式能以這麼直接的方式實現它:
class NLComponent {
public:
// declaration of virtual copy constructor
virtual NLComponent * clone() const = 0;
...
};
class TextBlock: public NLComponent {
public:
virtual TextBlock * clone() const // virtual copy
{ return new TextBlock(*this); } // constructor
...
};
class Graphic: public NLComponent {
public:
virtual Graphic * clone() const // virtual copy
{ return new Graphic(*this); } // constructor
...
};
正如我們看到的,類的虛擬拷貝建構函式只是呼叫它們真正的拷貝建構函式。因此“拷貝”的含義與真正的拷貝建構函式相同。如果真正的拷貝建構函式只做了簡單的拷貝,那麼虛擬拷貝建構函式也做簡單的拷貝。如果真正的拷貝建構函式做了全面的拷貝,那麼虛擬拷貝建構函式也做全面的拷貝。如果真正的拷貝建構函式做一些奇特的事情,象引用計數或copy-on-write(參見條款M29),那麼虛擬建構函式也這麼做。完全一致,太棒了。
注意上述程式碼的實現利用了最近才被採納的較寬鬆的虛擬函式返回值型別規則。被派生類重定義的虛擬函式不用必須與基類的虛擬函式具有一樣的返回型別。如果函式的返回型別是一個指向基類的指標(或一個引用),那麼派生類的函式可以返回一個指向基類的派生類的指標(或引用)。這不是C++的型別檢查上的漏洞,它使得有可能宣告象虛擬建構函式這樣的函式。這就是為什麼TextBlock的clone函式能夠返回TextBlock*和Graphic的clone能夠返回Graphic*的原因,即使NLComponent的clone返回值型別為NLComponent*。
在NLComponent中的虛擬拷貝建構函式能讓實現NewLetter的(正常的)拷貝建構函式變得很容易:
class NewsLetter {
public:
NewsLetter(const NewsLetter& rhs);
...
private:
list<NLComponent*> components;
};
NewsLetter::NewsLetter(const NewsLetter& rhs)
{
// 遍歷整個rhs連結串列,使用每個元素的虛擬拷貝建構函式
// 把元素拷貝進這個物件的component連結串列。
// 有關下面程式碼如何執行的詳細情況,請參見條款M35.
for (list<NLComponent*>::const_iterator it =
rhs.components.begin();
it != rhs.components.end();
++it) {
// "it" 指向rhs.components的當前元素,呼叫元素的clone函式,
// 得到該元素的一個拷貝,並把該拷貝放到
// 這個物件的component連結串列的尾端。
components.push_back((*it)->clone());
}
}
如果你對標準模板庫(STL)不熟悉,這段程式碼可能有些令人費解,不過原理很簡單:遍歷被拷貝的NewsLetter物件中的整個component連結串列,呼叫連結串列內每個元素物件的虛擬建構函式。我們在這裡需要一個虛擬建構函式,因為連結串列中包含指向NLComponent物件的指
針,但是我們知道其實每一個指標不是指向TextBlock物件就是指向Graphic物件。無論它指向誰,我們都想進行正確的拷貝操作,虛擬建構函式能夠為我們做到這點。
虛擬化非成員函式
就象建構函式不能真的成為虛擬函式一樣,非成員函式也不能成為真正的虛擬函式(參見Effective C++ 條款19)。然而,既然一個函式能夠構造出不同型別的新物件是可以理解的,那麼同樣也存在這樣的非成員函式,可以根據引數的不同動態型別而其行為特性也不同。例如,假設你想為TextBlock和Graphic物件實現一個輸出操作符。顯而易見的方法是虛擬化這個輸出操作符。但是輸出操作符是operator<<,函式把ostream&做為它的左引數(left-hand argument)(即把它放在函式引數列表的左邊 譯者注),這就不可能使該函式成為TextBlock 或 Graphic成員函式。
(這樣做也可以,不過看一看會發生什麼:
class NLComponent {
public:
// 對輸出操作符的不尋常的宣告
virtual ostream& operator<<(ostream& str) const = 0;
...
};
class TextBlock: public NLComponent {
public:
// 虛擬輸出操作符(同樣不尋常)
virtual ostream& operator<<(ostream& str) const;
};
class Graphic: public NLComponent {
public:
// 虛擬輸出操作符 (讓就不尋常)
virtual ostream& operator<<(ostream& str) const;
};
TextBlock t;
Graphic g;
...
t << cout; // 通過virtual operator<<
//把t列印到cout中。
// 不尋常的語法
g << cout; //通過virtual operator<<
//把g列印到cout中。
//不尋常的語法
類的使用者得把stream物件放到<<符號的右邊,這與輸出操作符一般的用發相反。為了能夠回到正常的語法上來,我們必須把operator<<移出TextBlock 和 Graphic類,但是如果我們這樣做,就不能再把它宣告為虛擬了。)
另一種方法是為列印操作宣告一個虛擬函式(例如print)把它定義在TextBlock 和 Graphic類裡。但是如果這樣,列印TextBlock 和 Graphic物件的語法就與使用operator<<做為輸出操作符的其它型別的物件不一致了,這些解決方法都不很令人滿意。我們想要的是一個稱為operator<<的非成員函式,其具有象print虛擬函式的行為特性。有關我們想要什麼的描述實際上已經很接近如何得到它的描述。我們定義operator<< 和print函式,讓前者呼叫後者!
class NLComponent {
public:
virtual ostream& print(ostream& s) const = 0;
...
};
class TextBlock: public NLComponent {
public:
virtual ostream& print(ostream& s) const;
...
};
class Graphic: public NLComponent {
public:
virtual ostream& print(ostream& s) const;
...
};
inline
ostream& operator<<(ostream& s, const NLComponent& c)
{
return c.print(s);
}
具有虛擬行為的非成員函式很簡單。你編寫一個虛擬函式來完成工作,然後再寫一個非虛擬函式,它什麼也不做只是呼叫這個虛擬函式。為了避免這個句法花招引起函式呼叫開銷,你當然可以內聯這個非虛擬函式(參見Effective C++ 條款33)。
現在你知道如何根據它們的一個引數讓非成員函式虛擬化,你可能想知道是否可能讓它們根據一個以上的引數虛擬化呢?可以,但是不是很容易。有多困難呢?參見條款M31;它將專門論述這個問題。