More Effective C++ 條款5 (轉)

gugu99發表於2008-03-01
More Effective C++ 條款5 (轉)[@more@]

 條款5:謹慎定義型別轉換:namespace prefix = o ns = "urn:schemas--com::office" />

C++能夠在兩種資料型別之間進行隱式轉換(implicit conversions),它繼承了C語言的轉換方法,例如允許把char隱式轉換為int和從short隱式轉換為double。因此當你把一個short值傳遞給準備接受double引數值的函式時,依然可以成功執行。C中許多這種可怕的轉換可能會導致資料的丟失,它們在C++中依然存在,包括int到short的轉換和double到char的轉換。

 

你對這些型別轉換是無能為力的,因為它們是語言本身的特性。不過當你增加自己的型別時,你就可以有更多的控制力,因為你能選擇是否提供函式讓編譯器進行隱式型別轉換。

 

有兩種函式允許編譯器進行這些的轉換:單引數建構函式(single-argument constructors和隱式型別轉換運算子。單引數建構函式是指只用一個引數即可以的建構函式。該函式可以是隻定義了一個引數,也可以是雖定義了多個引數但第一個引數以後的所有引數都有預設值。以下有兩個例子:

 

class Name {  // for names of things

public:

  Name(const string& s);  // 轉換 string 到

  // Name

  ...

 

};

 

class Rational {  // 有理數類

public:

  Rational(int numerator = 0,  // 轉換int到

  int denominator = 1);  // 有理數類

  ...

 

};

 

隱式型別轉換運算子只是一個樣子奇怪的成員函式:operator 關鍵字,其後跟一個型別符號。你不用定義函式的返回型別,因為返回型別就是這個函式的名字。例如為了允許Rational(有理數)類隱式地轉換為double型別(在用有理數進行混合型別運算時,可能有用),你可以如此宣告Rational類:

 

class Rational {

public:

  ...

  operator double() const;  // 轉換Rational類成

};  // double型別

 

在下面這種情況下,這個函式會被自動呼叫:

Rational r(1, 2);  // r 的值是1/2

 

double d = 0.5 * r;  // 轉換 r 到double,

   // 然後做乘法

 

以上這些說明只是一個複習,我真正想說的是為什麼你不需要定義各中型別轉換函式。

 

根本問題是當你在不需要使用轉換函式時,這些的函式缺卻能被呼叫執行。結果這些不正確的會做出一些令人惱火的事情,而你又很難判斷出原因。

 

讓我們首先分析一下隱式型別轉換運算子,它們是最容易處理的。假設你有一個如上所述的Rational類,你想讓該類擁有列印有理數的功能,就好像它是一個內建型別。因此,你可能會這麼寫:

 

Rational r(1, 2);

 

cout << r;   // 應該列印出"1/2"

 

再假設你忘了為Rational物件定義operator<

 

解決方法是用等同的函式來替代轉換運算子,而不用語法關鍵字。例如為了把Rational物件轉換為double,用asDouble函式代替operator double函式:

 

class Rational {

public:

  ...

  double asDouble() const;  //轉變 Rational

};   // 成double

 

這個成員函式能被顯式呼叫:

 

Rational r(1, 2);

 

cout << r;  // 錯誤! Rationa物件沒有

  // operator<<

 

cout << r.asDouble();  // 正確, 用double型別   //列印r

 

在多數情況下,這種顯式轉換函式的使用雖然不方便,但是函式被悄悄呼叫的情況不再會發生,這點損失是值得的。一般來說,越有的C++程式設計師就越喜歡避開型別轉換運算子。例如在C++標準庫(參見條款49和35)委員會工作的人員是在此領域最有經驗的,他們加在庫函式中的string型別沒有包括隱式地從string轉換成C風格的char*的功能,而是定義了一個成員函式c_str用來完成這個轉換,這是巧合麼?我看不是。

 

透過單引數建構函式進行隱式型別轉換更難消除。而且在很多情況下這些函式所導致的問題要甚於隱式型別轉換運算子。

 

舉一個例子,一個array類别範本,這些陣列需要呼叫者確定邊界的上限與下限:

 

template

class Array {

public:

  Array(int lowBound, int highBound);

  Array(int size);

 

  T& operator[](int index);

 

  ...

 

};

 

第一個建構函式允許呼叫者確定陣列的範圍,例如從10到20。它是一個兩引數建構函式,所以不能做為型別轉換函式。第二個建構函式讓呼叫者僅僅定義陣列元素的個數(使用方法與內建陣列的使用相似),不過不同的是它能做為型別轉換函式使用,能導致無窮的痛苦。

 

例如比較Array物件,部分程式碼如下:

 

bool operator==( const Array& lhs,

  const Array& rhs);

 

Array a(10);

Array b(10);

 

...

 

for (int i = 0; i < 10; ++i)

  if (a == b[i]) {   // 哎呦! "a" 應該是 "a[i]"

  do something for when

  a[i] and b[i] are equal;

  }

  else {

  do something for when they're not;

  }

 

我們想用a的每個元素與b的每個元素相比較,但是當錄入a時,我們偶然忘記了陣列下標。當然我們希望編譯器能報出各種各樣的警告資訊,但是它根本沒有。因為它把這個呼叫看成用Array引數(對於a)和int (對於b[i])引數呼叫operator==函式 ,然而沒有operator==函式是這些的引數型別,我們的編譯器注意到它能透過呼叫Array建構函式能轉換int型別到Array型別,這個建構函式只有一個int 型別的引數。然後編譯器如此去編譯,生成的程式碼就象這樣:

 

for (int i = 0; i < 10; ++i)

  if (a == static_cast< Array >(b[i]))  ...

每一次迴圈都把a的內容與一個大小為b[i]的臨時陣列(內容是未定義的)比較 。這不僅不可能以正確的方法執行,而且還是低下的。因為每一次迴圈我們都必須建立和釋放Array物件(見條款19)。

 

透過不宣告運算子(operator)的方法,可以克服隱式型別轉換運算子的缺點,但是單引數建構函式沒有那麼簡單。畢竟,你確實想給呼叫者提供一個單引數建構函式。同時你也希望防止編譯器不加鑑別地呼叫這個建構函式。幸運的是,有一個方法可以讓你魚肉與熊掌兼得。事實上是兩個方法:一是容易的方法,二是當你的編譯器不支援容易的方法時所必須使用的方法。

 

容易的方法是利用一個最新編譯器的特性,explicit關鍵字。為了解決隱式型別轉換而特別引入的這個特性,它的使用方法很好理解。建構函式用explicit宣告,如果這樣做,編譯器會拒絕為了隱式型別轉換而呼叫建構函式。顯式型別轉換依然合法:

 

template

class Array {

public:

  ...

  explicit Array(int size);  // 注意使用"explicit"

  ...

};

 

Array a(10);   // 正確, explicit 建構函式

  // 在建立物件時能正常使用

 

Array b(10);  // 也正確

 

if (a == b[i]) ...  // 錯誤! 沒有辦法

  // 隱式轉換

  // int 到 Array

 

if (a == Array(b[i])) ...  // 正確,顯式從int到

  // Array轉換

  // (但是程式碼的邏輯

   // 不合理)

 

if (a == static_cast< Array >(b[i])) ...

  // 同樣正確,同樣

  // 不合理

 

if (a == (Array)b[i]) ...  //C風格的轉換也正確,

  // 但是邏輯

   // 依舊不合理

 

 

在例子裡使用了static_cast(參見條款2),兩個“>”字元間的空格不能漏掉,如果這樣寫語句:

 

if (a == static_cast>(b[i])) ...

 

這是一個不同的含義的語句。因為C++編譯器把”>>”做為一個符號來解釋。在兩個”>”間沒有空格,語句會產生語法錯誤。

 

如果你的編譯器不支援explicit,你不得不回到不使用成為隱式型別轉換函式的單引數建構函式。(……)

 

我前面說過複雜的規則決定哪一個隱式型別轉換是合法的,哪一個是不合法的。這些規則中沒有一個轉換能夠包含自定義型別(呼叫單引數建構函式或隱式型別轉換運算子)。你能利用這個規則來正確構造你的類,使得物件能夠正常構造,同時去掉你不想要的隱式型別轉換。

 

再來想一下陣列模板,你需要用整形變數做為建構函式引數來確定陣列大小,但是同時又必須防止從整數型別到臨時陣列物件的隱式型別轉換。你要達到這個目的,先要建立一個新類ArraySize。這個物件只有一個目的就是表示將要建立陣列的大小。你必須修改Array的單引數建構函式,用一個ArraySize物件來代替int。程式碼如下:

 

template

class Array {

public:

 

  class ArraySize {  // 這個類是新的

  public:

  ArraySize(int numElements): theSize(numElements) {}

  int size() const { return theSize; }

 

private:

  int theSize;

  };

 

Array(int lowBound, int highBound);

  Array(ArraySize size);  // 注意新的宣告

 

...

 

};

 

 

這裡把ArraySize巢狀入Array中,為了強調它總是與Array一起使用。你也必須宣告ArraySize為公有,為了讓任何人都能使用它。

 

想一下,當透過單引數建構函式定義Array物件,會發生什麼樣的事情:

 

Array a(10);

 

你的編譯器要求用int引數呼叫Array裡的建構函式,但是沒有這樣的建構函式。編譯器意識到它能從int引數轉換成一個臨時ArraySize物件,ArraySize物件只是Array建構函式所需要的,這樣編譯器進行了轉換。函式呼叫(及其後的物件建立)也就成功了。

 

事實上你仍舊能夠安心地構造Array物件,不過這樣做能夠使你避免型別轉換。考慮一下以下程式碼:

 

bool operator==( const Array& lhs,

  const Array& rhs);

Array a(10);

Array b(10);

...

for (int i = 0; i < 10; ++i)

  if (a == b[i]) ...  // 哎呦! "a" 應該是 "a[i]";

  // 現在是一個錯誤。

 

為了呼叫operator==函式,編譯器要求Array物件在”==”右側,但是不存在一個引數為int的單引數建構函式。而且編譯器無法把int轉換成一個臨時ArraySize物件然後透過這個臨時物件建立必須的Array物件,因為這將呼叫兩個使用者定義(user-defined)的型別轉換,一個從int到ArraySize,一個從ArraySize到Array。這種轉換順序被禁止的,所以當試圖進行比較時編譯器肯定會產生錯誤。

 

ArraySize類的使用有些象一個有目的的幫手,這是一個更通用技術的應用例項。類似於ArraySize的類經常被稱為 classes,因為這樣類的每一個物件都為了支援其他物件的工作。ArraySize物件實際是一個整數型別的替代者,用來在建立Array物件時確定陣列大小。Proxy物件能幫你更好地控制的在某些方面的行為,否則你就不能控制這些行為,比如在上面的情況裡,這種行為是指隱式型別轉換,所以它值得你去學習和使用。你可能會問你如何去學習它呢?一種方法是轉向條款33;它專門討論proxy classes。

 

在你跳到條款33之前,再仔細考慮一下本條款的內容。讓編譯器進行隱式型別轉換所造成的弊端要大於它所帶來的好處,所以除非你確實需要,不要定義型別轉換函式。


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

相關文章