More Effective C++ 條款28(中) (轉)

gugu99發表於2008-04-27
More Effective C++ 條款28(中) (轉)[@more@]

 條款28:靈巧(smart)指標(中):namespace prefix = o ns = "urn:schemas--com::office" />

目前為止我們討論的能讓我們建立、釋放、複製、賦值、dereference靈巧指標。但是有一件我們做不到的事情是“發現靈巧指標為NULL”:

SmartPtr ptn;

 

...

 

if (ptn == 0) ...  // error!

 

if (ptn) ...  // error!

 

if (!ptn) ...  // error!

這是一個嚴重的限制。

在靈巧指標類里加入一個isNull成員函式是一件很容易的事,但是仍然沒有解決當測試NULL時靈巧指標的行為與dumb pointer不相似的問題。另一種方法是提供隱式型別轉換運算子,允許編譯上述的測試。一般應用於這種目的的型別轉換是void* :

template

class SmartPtr {

public:

  ...

  operator void*();  // 如果靈巧指標為null,

  ...  // 返回0,否則返回

};   // 非0。

 

SmartPtr ptn;

 

...

 

if (ptn == 0) ...  // 現在正確

 

if (ptn) ...  // 也正確

 

if (!ptn) ...  // 正確

這與iostream類中提供的型別轉換相同,所以可以這樣編寫程式碼:

ifstream inputFile("datafile.dat");

 

if (inputFile) ...  // 測試inputFile是否已經被

  // 成功地開啟。

象所有的型別轉換函式一樣,它有一個缺點,在一些情況下雖然大多數員希望它失敗,但是函式還能夠成功地被呼叫(參見條款5)。特別是它允許靈巧指標與完全不同的型別之間進行比較:

SmartPtr pa;

SmartPtr po;

 

...

 

if (pa == po) ...  // 這能夠被成功編譯!

即使在SmartPtrSmartPtr之間沒有operator= 函式,也能夠編譯,因為靈巧指標被隱式地轉換為void*指標,對於內建指標型別有一個內建的比較函式。這種進行隱式型別轉換的行為特性很危險。(再看一下條款5,必須反反覆覆地閱讀,做到耳熟能詳。)

在void*型別轉換方面,也有一些變通之策。有些設計者採用到const void*的型別轉換,還有一些採取轉換到bool的方法。這些變通之策都沒有消除混合型別比較的問題。

有一種兩全之策可以提供合理的測試空值的語法形式,同時把不同型別的靈巧指標之間進行比較的可能性降到最低。這就是在靈巧指標類中過載operator!,當且僅當靈巧指標是一個空指標時,operator!返回true:

template

class SmartPtr {

public:

  ...

  bool operator!() const;  // 當且僅當靈巧指標是

  ...   // 空值,返回true。

 

};

客戶端程式如下所示:

SmartPtr ptn;

 

...

 

if (!ptn) {  // 正確

  ...  // ptn 是空值

}

else {

  ...  // ptn不是空值

}

但是這樣就不正確了:

if (ptn == 0) ...   // 仍然錯誤

 

if (ptn) ...  // 也是錯誤的

 

僅在這種情況下會存在不同型別之間進行比較:

SmartPtr pa;

SmartPtr po;

 

...

 

if (!pa == !po) ...  // 能夠編譯

幸好程式設計師不會經常這樣編寫程式碼。有趣的是,iostream庫的實作除了提供void*隱式的型別轉換,也有operator!函式,不過這兩個函式被用於測試的流狀態有些不同。(在C++類庫標準中(參見Effective C++ 條款49和本書條款35),void*隱式的型別轉換已經被bool型別的轉換所替代,operator bool總是返回與operator!相反的值。)

把靈巧指標轉變成dumb指標

有時你要在一個程式裡或已經使用dumb指標的程式庫中新增靈巧指標。例如,你的分散式原來不是分散式的,所以可能有一些老式的庫函式沒有使用靈巧指標:

class Tuple { ... };  // 同上

 

void normalize(Tuple *pt);  // 把*pt 放入

  // 正規化中; 注意使用的

  // 是dumb指標

考慮一下,如果你試圖用指向Tuple的靈巧指標呼叫normalize,會出現什麼情況:

Ptr pt;

 

...

 

normalize(pt);  // 錯誤!

 

這種呼叫不能夠編譯,因為不能把DBPtr轉換成Tuple*。你可以這樣做,從而使該該函式正常執行:

normalize(&*pt);  // 繁瑣, 但合法

不過我覺得你會討厭這種呼叫方式。

在靈巧指標模板中增加指向T的dumb指標的隱式型別轉換運算子,可以讓以上函式呼叫成功執行:

template  // 同上

class DBPtr {

public:

  ...

  operator T*() { return pointee; }

  ...

};

 

DBPtr pt;

 

...

 

normalize(pt);  // 能夠執行

 

並且這個函式也消除了測試空值的問題:

if (pt == 0) ...  // 正確, 把pt轉變成

  // Tuple*

 

if (pt) ...  // 同上

 

if (!pt) ...  // 同上 (reprise)

然而,它也有型別轉換函式所具有的缺點(幾乎總是這樣,看條款5)。它使得客戶端能夠很容易地直接訪問dumb指標,繞過“類指標(pointer-like)”所提供的“靈巧”特性:

void processTuple(DBPtr& pt)

{

  Tuple *rawTuplePtr = pt;  // 把DBPtr 轉變成

   // Tuple*

 

  使用raw TuplePtr 修改 tuple;

 

}

通常,靈巧指標提供的“靈巧”行為特性是設計中的主要組成部分,所以允許客戶端使用dumb指標會導致災難性的後果。例如,如果DBPtr實現了條款29中引用計數的功能,允許客戶端直接對dumb指標進行操作很可能破壞“引用計數”資料結構,而導致引用計數錯誤。

甚至即使你提供一個從靈巧指標到dumb指標的隱式轉換運算子,靈巧指標也不能真正地做到與dumb指標互換。因為從靈巧指標到dumb指標的轉換是“定義型別轉換”,在同一時間進行這種轉換的次數不能超過一次。例如假設有一個表示能夠訪問某一元組的所有客戶的類:

class TupleAccessors {

public:

  TupleAccessors(const Tuple *pt);  // pt ntifies the

  ...  // tuple whose accessors

};  // we care about

通常,TupleAccessors的單引數建構函式也可以做為從Tuple*到TupleAccessors的型別轉換運算子(參見條款5)。現在考慮一下用於合併兩個TupleAccessors物件內資訊的函式:

TupleAccessors merge(const TupleAccessors& ta1,

  const TupleAccessors& ta2);

因為一個Tuple*可以被隱式地轉換為TupleAccessors,用兩個dumb Tuple*呼叫merge函式,可以正常執行:

Tuple *pt1, *pt2;

 

...

 

merge(pt1, pt2);  // 正確, 兩個指標被轉換為

  // TupleAccessors s

如果用靈巧指標DBPtr進行呼叫,編譯就會失敗:

DBPtr pt1, pt2;

 

...

 

merge(pt1, pt2);   // 錯誤!不能把 pt1 和

  // pt2轉換稱TupleAccessors物件

因為從DBPtr到TupleAccessors的轉換要呼叫兩次使用者定義型別轉換(一次從DBPtr到Tuple*,一次從Tuple*到TupleAccessors),編譯器不會進行這種轉換序列。

提供到dumb指標的隱式型別轉換的靈巧指標類也暴露了一個非常有害的。考慮這個程式碼:

DBPtr pt = new Tuple;

 

...

 

delete pt;

這段程式碼應該不能被編譯,pt不是指標,它是一個物件,你不能刪除一個物件。只有指標才能被刪除,對麼?

當然對了。但是回想一下條款5:編譯器使用隱式型別轉換來儘可能使函式呼叫成功,再回想一下條款8:使用delete會呼叫解構函式和operator delete,兩者都是函式。編譯器欲使在delete語句裡的兩個函式成功呼叫,就把pt隱式轉換為Tuple*,然後刪除它。這樣做必然會破壞你的程式。

如果pt擁有它指向的物件,物件就會被刪除兩次,一次在呼叫delete時,第二次在pt的解構函式被呼叫時。如果pt不擁有物件,而是其他人擁有,擁有者可以刪除pt,但是如果pt指向物件的擁有者不是刪除pt的人,有刪除權的擁有者以後還會再次刪除該物件。不論是前者所述的情況還是後者的情況都會導致一個物件被刪除兩次,這樣做會產生不能預料的後果。

這個bug極為有害,因為隱藏在靈巧指標後面的全部思想就是讓它們不論是在外觀上還是在使用感覺上都與dumb指標儘可能地相似。你越接近這種思想,你的客戶端就越可能忘記正在使用靈巧指標。如果他們忘記了正在使用靈巧指標,肯定會在呼叫new之後呼叫delete,以防止資源洩漏,誰又能責備他們這樣做不對呢?

底線很簡單:除非有一個讓人非常信服的原因去這樣做,否則絕對不要提供轉換到dumb指標的隱式型別轉換運算子。

靈巧指標和繼承類到基類的型別轉換

假設我們有一個public繼承層次結構,以模型化商店的商品:

class MusicProduct {

public:

  MusicProduct(const string& title);

  virtual void play() const = 0;

  virtual void displayTitle() const = 0;

  ...

};

 

class Cassette: public MusicProduct {

public:

  Cassette(const string& title);

  virtual void play() const;

  virtual void displayTitle() const;

  ...

};

 

class CD: public MusicProduct {

public:

  CD(const string& title);

  virtual void play() const;

  virtual void displayTitle() const;

  ...

};

 

再接著假設,我們有一個函式,給它一個MusicProduct物件,它能顯示產品名,並它:

void displayAndPlay(const MusicProduct* pmp, int numTimes)

{

  for (int i = 1; i <= numTimes; ++i) {

  pmp->displayTitle();

  pmp->play();

  }

}

這個函式能夠這樣使用:

Cassette *funMusic = new Cassette("Alapalooza");

CD *nightmareMusic = new CD("Di Hits of the 70s");

 

displayAndPlay(funMusic, 10);

displayAndPlay(nightmareMusic, 0);

這並沒有什麼值得驚訝的東西,但是當我們用靈巧指標替代dumb指標,會發生什麼呢:

void displayAndPlay(const SmartPtr& pmp,

  int numTimes);

 

SmartPtr funMusic(new Cassette("Alapalooza"));

SmartPtr nightmareMusic(new CD("Disco Hits of the 70s"));

 

displayAndPlay(funMusic, 10);  // 錯誤!

displayAndPlay(nightmareMusic, 0);  // 錯誤!

如果靈巧指標這麼聰明,為什麼不能編譯這些程式碼呢?

不能進行編譯原因是不能把SmartPtr或SmartPtr轉換成SmartPtr。從編譯器的觀點來看,這些類之間沒有任何關係。為什麼編譯器的會這樣認為呢?畢竟SmartPtrSmartPtr不是從SmartPtr繼承過來的,這些類之間沒有繼承關係,我們不可能要求編譯器把一種物件轉換成另一種型別的物件。

幸運的是,有辦法避開這種限制,這種方法的核心思想(不是實際操作)很簡單:對於可以進行隱式轉換的每個靈巧指標類都提供一個隱式型別轉換運算子(參見條款5)。例如在music類層次內,在Cassette和CD的靈巧指標類內你可以加入SmartPtr函式:

class SmartPtr {

public:

  operator SmartPtr()

  { return SmartPtr(pointee); }

 

  ...

 

private:

  Cassette *pointee;

};

 

class SmartPtr {

public:

  operator SmartPtr()

  { return SmartPtr(pointee); }

 

  ...

 

private:

  CD *pointee;

};

這種方法有兩個缺點。第一,你必須人為地特化(specialize)SmartPtr類,所以你加入隱式型別轉換運算子也就破壞了模板的通用性。第二,你可能必須新增許多型別轉換符,因為你指向的物件可以位於繼承層次中很深的位置,你必須為直接或間接繼承的每一個基類提供一個型別轉換符。(如果你想你能夠克服這個缺點,方法是僅僅為轉換到直接基類而提供一個隱式型別轉換符,那麼你再想想這樣做行麼?因為編譯器在同一時間呼叫使用者定義型別轉換函式的次數不能超過一次,它們不能把指向T的靈巧指標轉換為指向T的間接基類的靈巧指標,除非只要一步就能完成。)

如果你能讓編譯器為你編寫所有的型別轉換函式,這會節省很多時間。感謝最近的語言擴充套件,讓你能夠做到,這個擴充套件能宣告(非虛)成員函式模板(通常就叫成員模板(member template)),你能使用它來生成靈巧指標型別轉換函式,如下:

template  // 模板類,指向T的

class SmartPtr {  // 靈巧指標

public:

  SmartPtr(T* realPtr = 0);

 

  T* operator->() const;

  T& operator*() const;

 

  template  // 模板成員函式

  operator SmartPtr()  // 為了實現隱式型別轉換.

  {

  return SmartPtr(pointee);

  }

 

  ...

};

現在請你注意,這可不是魔術——不過也很接近於魔術。它的原理如下所示。(如果下面的內容讓你感到既冗長又令你費解,請不要失望,一會兒我會給出一個例子。我保證你看完例子後,就能夠更深入地理解這段內容了)假設編譯器有一個指向T物件的靈巧指標,它要把這個物件轉換成指向“T的基類”的靈巧指標。編譯器首先檢查SmartPtr的類定義,看其有沒有宣告必須的型別轉換符,但是它沒有宣告。(這不是指:在模板上面沒有宣告型別轉換符)編譯器然後檢查是否存在一個成員函式模板,可以被例項化,用來進行它所期望的型別轉換。它發現了一個這樣的模板(帶有形式型別引數newType),所以它把newType繫結到T的基類型別上,來例項化模板。這時,惟一一個問題是例項化的成員函式程式碼能否被編譯。傳遞(dumb)指標pointee到指向“T的基類”的靈巧指標的建構函式,這個語句是合法的,把它轉變成指向其基類(public 或 protected)物件的指標也必然是合法的,因此型別轉換運算子能夠被編譯,可以成功地把指向T的靈巧指標隱式地型別轉換為指向“T的基類”的靈巧指標。

舉一個例子會有所幫助。讓我們回到CDs、cassettes、music產品的繼承層次上來。我們先前已經知道下面這段程式碼不能被編譯,因為編譯器不能把指向CD的靈巧指標轉換為指向music產品的靈巧指標:

void displayAndPlay(const SmartPtr& pmp,

  int howMany);

 

SmartPtr funMusic(new Cassette("Alapalooza"));

SmartPtr nightmareMusic(new CD("Disco Hits of the 70s"));

 

displayAndPlay(funMusic, 10);  // 以前是一個錯誤

displayAndPlay(nightmareMusic, 0);  // 以前是一個錯誤

修改了靈巧指標類,包含了隱式型別轉換運算子的成員函式模板以後,這個程式碼就可以成功執行了。拿如下呼叫舉例,看看為什麼能夠成功執行:

displayAndPlay(funMusic, 10);

funMusic物件的型別是SmartPtr。函式displayAndPlay期望的引數是SmartPtr地物件。編譯器偵測到型別不匹配並尋找把funMusic轉換成SmartPtr物件的方法。它在SmartPtr類裡尋找帶有SmartPtr型別引數的單引數建構函式(參見條款5),但是沒有找到。然後它們又尋找成員函式模板,能被例項化產生這樣的函式。它們在SmartPtr發現了模板,把newType繫結到MusicProduct上,生成必須的函式。例項化函式,生成這樣的程式碼:

SmartPtr::  operator SmartPtr()

{

  return SmartPtr(pointee);

}

能編譯這行程式碼麼?實際上這段程式碼就是用pointee做為引數呼叫SmartPtr的建構函式,所以真正的問題是能否用一個Cassette*指標構造一個SmartPtr物件,現在我們對dumb指標型別之間的轉換已經很熟悉了,Cassette*能夠被傳遞給需要MusicProduct*指標的地方。因此SmartPtr建構函式可以成功呼叫,同樣SmartPtrSmartPtr之間的型別轉換也能成功進行。太棒了,實現了靈巧指標之間的型別轉換,還有什麼比這更簡單麼?

 而且,還有什麼比這功能更強大麼?不要被這個例子誤導,而認為這種方法只能用於把指標在繼承層次中向上進行型別轉換。這種方法可以成功地用於任何合法的指標型別轉換。如果你有dumb指標T1*和另一種dumb指標T2*,當且僅當你能隱式地把T1*轉換為T2*時,你就能夠隱式地把指向T1的靈巧指標型別轉換為指向T2的靈巧指標型別。

 


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10748419/viewspace-1003056/,如需轉載,請註明出處,否則將追究法律責任。

相關文章