天天看點

《More Effective C++》 條款5 謹慎定義類型轉換函數

---恢複内容開始---

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 { // 有理數類

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

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

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

class rational {

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

}; // double類型

在下面這種情況下,這個函數會被自動調用:

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

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

// 然後做乘法

 完整示例:

《More Effective C++》 條款5 謹慎定義類型轉換函數
《More Effective C++》 條款5 謹慎定義類型轉換函數

view code

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

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

讓我們首先分析一下隐式類型轉換運算符,它們是最容易處理的。假設你有一個如上所述的rational類,你想讓該類擁有列印有理數對象的功能,就好像它是一個内置類型。是以,你可能會這麼寫:

rational r(1, 2);

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

再假設你忘了為rational對象定義operator<<。你可能想列印操作将失敗,因為沒有合适的的operator<<被調用。但是你錯了。當編譯器調用operator<<時,會發現沒有這樣的函數存在,但是它會試圖找到一個合适的隐式類型轉換順序以使得函數調用正常運作。類型轉換順序的規則定義是複雜的,但是在這種情況下編譯器會發現它們能調用rational::operator double函數,來把r轉換為double類型。是以上述代碼列印的結果是一個浮點數,而不是一個有理數。這簡直是一個災難,但是它表明了隐式類型轉換的缺點:它們的存在将導緻錯誤的發生。

解決方法是用等同的函數來替代轉換運算符,而不用文法關鍵字。例如為了把rational對象轉換為double,用asdouble函數代替operator double函數:

double asdouble() const; //轉變 rational

}; // 成double

這個成員函數能被顯式調用:

cout << r; // 錯誤! rationa對象沒有

// operator<<

cout << r.asdouble(); // 正确, 用double類型 //列印r

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

通過單參數構造函數進行隐式類型轉換更難消除。而且在很多情況下這些函數所導緻的問題要甚于隐式類型轉換運算符。

舉一個例子,一個array類模闆,這些數組需要調用者确定邊界的上限與下限:

template<class t>

class array {

array(int lowbound, int highbound);

array(int size);

t& operator[](int index);

第一個構造函數允許調用者确定數組索引的範圍,例如從10到20。它是一個兩參數構造函數,是以不能做為類型轉換函數。第二個構造函數讓調用者僅僅定義數組元素的個數(使用方法與内置數組的使用相似),不過不同的是它能做為類型轉換函數使用,能導緻無窮的痛苦。

例如比較array<int>對象,部分代碼如下:

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]"

do something for when

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

}

else {

do something for when they're not;

我們想用a的每個元素與b的每個元素相比較,但是當錄入a時,我們偶然忘記了數組下标。當然我們希望編譯器能報出各種各樣的警告資訊,但是它根本沒有。因為它把這個調用看成用array<int>參數(對于a)和int (對于b[i])參數調用operator==函數 ,然而沒有operator==函數是這些的參數類型,我們的編譯器注意到它能通過調用array<int>構造函數能轉換int類型到array<int>類型,這個構造函數隻有一個int 類型的參數。然後編譯器如此去編譯,生成的代碼就象這樣:

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

每一次循環都把a的内容與一個大小為b[i]的臨時數組(内容是未定義的)比較 。這不僅不可能以正确的方法運作,而且還是效率低下的。因為每一次循環我們都必須建立和釋放array<int>對象(見條款19)。

通過不聲明運算符(operator)的方法,可以克服隐式類型轉換運算符的缺點,但是單參數構造函數沒有那麼簡單。畢竟,你确實想給調用者提供一個單參數構造函數。同時你也希望防止編譯器不加鑒别地調用這個構造函數。幸運的是,有一個方法可以讓你魚肉與熊掌兼得。事實上是兩個方法:一是容易的方法,二是當你的編譯器不支援容易的方法時所必須使用的方法。

容易的方法是利用一個最新編譯器的特性,explicit關鍵字。為了解決隐式類型轉換而特别引入的這個特性,它的使用方法很好了解。構造函數用explicit聲明,如果這樣做,編譯器會拒絕為了隐式類型轉換而調用構造函數。顯式類型轉換依然合法:

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 == (array<int>)b[i]) ... //c風格的轉換也正确,

// 但是邏輯

// 依舊不合理

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

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

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

如果你的編譯器不支援explicit,你不得不回到不使用成為隐式類型轉換函數的單參數構造函數。(……)

我前面說過複雜的規則決定哪一個隐式類型轉換是合法的,哪一個是不合法的。這些規則中沒有一個轉換能夠包含使用者自定義類型(調用單參數構造函數或隐式類型轉換運算符)。你能利用這個規則來正确構造你的類,使得對象能夠正常構造,同時去掉你不想要的隐式類型轉換。

再來想一下數組模闆,你需要用整形變量做為構造函數參數來确定數組大小,但是同時又必須防止從整數類型到臨時數組對象的隐式類型轉換。你要達到這個目的,先要建立一個新類arraysize。這個對象隻有一個目的就是表示将要建立數組的大小。你必須修改array的單參數構造函數,用一個arraysize對象來代替int。代碼如下:

class arraysize { // 這個類是新的

arraysize(int numelements): thesize(numelements) {}

int size() const { return thesize; }

private:

int thesize;

array(arraysize size); // 注意新的聲明

這裡把arraysize嵌套入array中,為了強調它總是與array一起使用。你也必須聲明arraysize為公有,為了讓任何人都能使用它。

想一下,當通過單參數構造函數定義array對象,會發生什麼樣的事情:

你的編譯器要求用int參數調用array<int>裡的構造函數,但是沒有這樣的構造函數。編譯器意識到它能從int參數轉換成一個臨時arraysize對象,arraysize對象隻是array<int>構造函數所需要的,這樣編譯器進行了轉換。函數調用(及其後的對象建立)也就成功了。

事實上你仍舊能夠安心地構造array對象,不過這樣做能夠使你避免類型轉換。考慮一下以下代碼:

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

// 現在是一個錯誤。

為了調用operator==函數,編譯器要求array<int>對象在”==”右側,但是不存在一個參數為int的單參數構造函數。而且編譯器無法把int轉換成一個臨時arraysize對象然後通過這個臨時對象建立必須的array<int>對象,因為這将調用兩個使用者定義(user-defined)的類型轉換,一個從int到arraysize,一個從arraysize到array<int>。這種轉換順序被禁止的,是以當試圖進行比較時編譯器肯定會産生錯誤。

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

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

繼續閱讀