讓編譯器進行隱式類型轉換所造成的弊端要大於它所帶來的好處,所以除非你確實需要,不要定義類型轉換函數。
隱式類型轉換的缺點:它們的存在將導致錯誤的發生。
例如:
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"
當編譯器調用operator<<時,會發現沒有這樣的函數存在,但是它會試圖找到一個合適的隱式類型轉換順序以使得函數調用正常運行。類型轉換順序的規則定義是復雜的,但是在現在這種情況下,編譯器會發現它們能調用Rational::operatordouble函數來把r轉換為double類型。所以上述代碼打印的結果是一個浮點數,而不是一個有理數。這簡直是一個災難,但是它表明了隱式類型轉換的缺點:它們的存在將導致錯誤的發生。
解決方法是用不使用語法關鍵字的等同的函數來替代轉換運算符。例如為了把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
通過不聲明運算符(operator)的方法,可以克服隱式類型轉換運算符的缺點,但是單參數構造函數沒有那麼簡單。畢竟,你確實想給調用者提供一個單參數構造函數。同時你也希望防止編譯器不加鑒別地調用這個構造函數。幸運的是,有一個方法可以讓你魚肉與熊掌兼得。事實上是兩個方法:一是容易的方法,二是當你的編譯器不支持容易的方法時所必須使用的方法。
容易的方法是利用一個最新編譯器的特性,explicit關鍵字。為了解決隱式類型轉換而特別引入的這個特性,它的使用方法很好理解。構造函數用explicit聲明,如果這樣做,編譯器會拒絕為了隱式類型轉換而調用構造函數。顯式類型轉換依然合法:
template<class T>
class Array {
public:
...
explicit Array(int size); // 注意使用"explicit"
...
};
Array<int> a(10); // 正確, explicit 構造函數在建立對象時能正常使用
Array<int> b(10); // 也正確
if (a == b[i]) ... // 錯誤! 沒有辦法隱式轉換int 到 Array<int>
if (a == Array<int>(b[i])) ... // 正確,顯式從int到Array<int>轉換(但是代碼的邏輯不合理)
if (a == static_cast< Array<int> >(b[i])) ... //同樣正確,同樣不合理
if (a == (Array<int>)b[i]) ... //C風格的轉換也正確,但是邏輯 依舊不合理
在例子裡使用了static_cast(參見條款M2),兩個“>”字符間的空格不能漏掉,如果這樣寫語句:
if (a == static_cast<Array<int>>(b[i])) ...
這是一個不同的含義的語句。因為C++編譯器把“>>”做為一個符號來解釋。在兩個“>”間沒有空格,語句會產生語法錯誤。
如果你的編譯器不支持explicit,你不得不回到不使用成為隱式類型轉換函數的單參數構造函數。
我前面說過復雜的規則決定哪一個隱式類型轉換是合法的,哪一個是不合法的。這些規則中沒有一個轉換能夠包含用戶自定義類型(調用單參數構造函數或隱式類型轉換運算符)。你能利用這個規則來正確構造你的類,使得對象能夠正常構造,同時去掉你不想要的隱式類型轉換。
再來想一下數組模板,你需要用整形變量做為構造函數參數來確定數組大小,但是同時又必須防止從整數類型到臨時數組對象的隱式類型轉換。你要達到這個目的,先要建立一個新類ArraySize。這個對象只有一個目的就是表示將要建立數組的大小。你必須修改Array的單參數構造函數,用一個ArraySize對象來代替int。代碼如下:
template<class T>
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<int> a(10);
你的編譯器要求用int參數調用Array<int>裡的構造函數,但是沒有這樣的構造函數。編譯器意識到它能從int參數轉換成一個臨時ArraySize對象,ArraySize對象只是Array<int>構造函數所需要的,這樣編譯器進行了轉換。函數調用(及其後的對象建立)也就成功了。
如果你沒有Array(ArraySize size) 構造函數,在某些情況下也是有利的。考慮一下以下代碼:
bool operator==( const Array<int>& lhs,
const Array<int>& rhs);
Array<int> a(10);
Array<int> b(10);
...
for (int i = 0; i < 10; ++i)
if (a == b[i]) ... // 哎呦! "a" 應該是 "a[i]";
// 現在是一個錯誤。
為了調用operator==函數,編譯器要求Array<int>對象在”==”右側,但是如果不存在一個參數為int的單參數構造函數,那麼編譯器就無法把int轉換成一個臨時ArraySize對象然後通過這個臨時對象建立必須的Array<int>對象,所以當試圖進行比較時編譯器肯定會產生錯誤。