讀書筆記 effective c++ Item 24 如果函數的所有參數都需要類型轉換,將其聲明成非成員函數。本站提示廣大學習愛好者:(讀書筆記 effective c++ Item 24 如果函數的所有參數都需要類型轉換,將其聲明成非成員函數)文章只能為提供參考,不一定能成為您想要的結果。以下是讀書筆記 effective c++ Item 24 如果函數的所有參數都需要類型轉換,將其聲明成非成員函數正文
使類支持隱式轉換是一個壞的想法。當然也有例外的情況,最常見的一個例子就是數值類型。舉個例子,如果你設計一個表示有理數的類,允許從整型到有理數的隱式轉換應該是合理的。在C++內建類型中,從int轉換到double也是再合理不過的了(比從double轉換到int更加合理)。看下面的例子:
1 class Rational { 2 3 public: 4 5 Rational(int numerator = 0, // ctor is deliberately not explicit; 6 7 int denominator = 1); // allows implicit int-to-Rational 8 9 // conversions 10 11 int numerator() const; // accessors for numerator and 12 13 int denominator() const; // denominator — see Item 22 14 15 private: 16 17 ... 18 19 };
你想支持有理數的算術運算,比如加法,乘法等等,但是你不知道是通過成員函數還是非成員函數,或者非成員友元函數來實現。你的直覺會告訴你當你猶豫不決的時候,你應該使用面向對象的特性。有理數的乘積和有理數類相關,所有將有理數的operator*實現放在Rationl類中看上去是很自然的事。但違反直覺的是,Item 23已經論證過了將函數放在類中的方法有時候會違背面向對象法則,現在我們將其放到一邊,研究一下將operator*實現為成員函數的做法:
1 class Rational { 2 3 public: 4 5 ... 6 7 const Rational operator*(const Rational& rhs) const; 8 9 };
(如果你不明白為什麼函數聲明成上面的樣子——返回一個const value值,參數為const引用,參考Item 3,Item 20和Item21)
這個設計讓你極為方便的執行有理數的乘法:
1 Rational oneEighth(1, 8); 2 3 Rational oneHalf(1, 2); 4 5 Rational result = oneHalf * oneEighth; // fine 6 7 result = result * oneEighth; // fine
但是你不滿足。你希望可以支持混合模式的操作,例如可以支持int類型和Rational類型之間的乘法。這種不同類型之間的乘法也是很自然的事情。
當你嘗試這種混合模式的運算的時候,你會發現只有一半的操作是對的:
1 result = oneHalf * 2; // fine 2 3 result = 2 * oneHalf; // error!
這就不太好了,乘法是支持交換律的。
2. 問題出在哪裡?將上面的例子用等價的函數形式寫出來,你就會知道問題出在哪裡:
1 result = oneHalf.operator*(2); // fine 2 3 result = 2.operator*(oneHalf ); // error!
oneHalf對象是Rational類的一個實例,而Rational支持operator*操作,所以編譯器能調用這個函數。然而,整型2卻沒有關聯的類,也就沒有operator*成員函數。編譯器同時會去尋找非成員operator*函數(也就是命名空間或者全局范圍內的函數):
1 result = operator*(2, oneHalf ); // error!
但是在這個例子中,沒有帶int和Rational類型參數的非成員函數,所以搜索會失敗。
再看一眼調用成功的那個函數。你會發現第二個參數是整型2,但是Rational::operator*使用Rational對象作為參數。這裡發生了什麼?為什麼都是2,一個可以另一個卻不行?
沒錯,這裡發生了隱式類型轉換。編譯器知道函數需要Rational類型,但你傳遞了int類型的實參,它們也同樣知道通過調用Rational的構造函數,可以將你提供的int實參轉換成一個Rational類型實參,這就是編譯器所做的。它們的做法就像下面這樣調用:
1 const Rational temp(2); // create a temporary 2 3 // Rational object from 2 4 5 result = oneHalf * temp; // same as oneHalf.operator*(temp);
當然,編譯器能這麼做僅僅因為類提供了non-explicit構造函數。如果Rational類的構造函數是explicit的,下面的兩個句子都會出錯:
1 result = oneHalf * 2; // error! (with explicit ctor); 2 3 // can’t convert 2 to Rational 4 5 result = 2 * oneHalf; // same error, same problem
這樣就不能支持混合模式的運算了,但是至少兩個句子的行為現在一致了。
然而你的目標是既能支持混合模式的運算又要滿足一致性,也就是,你需要一個設計使得上面的兩個句子都能通過編譯。回到上面的例子,當Rational的構造函數是non-explicit的時候,為什麼一個能編譯通過另外一個不行呢?
看上去是這樣的,只有參數列表中的參數才有資格進行隱式類型轉換。而調用成員函數的隱式參數——this指針指向的那個——絕沒有資格進行隱式類型轉換。這就是為什麼第一個調用成功而第二個調用失敗的原因。
3. 解決方法是什麼?然而你仍然希望支持混合模式的算術運行,但是方法現在可能比較明了了:使operator*成為一個非成員函數,這樣就允許編譯器在所有的參數上面執行隱式類型轉換了:
1 class Rational { 2 3 ... // contains no operator* 4 5 }; 6 7 const Rational operator*(const Rational& lhs, // now a non-member 8 9 const Rational& rhs) // function 10 11 { 12 13 return Rational(lhs.numerator() * rhs.numerator(), 14 15 lhs.denominator() * rhs.denominator()); 16 17 } 18 19 Rational oneFourth(1, 4); 20 21 Rational result; 22 23 result = oneFourth * 2; // fine 24 25 result = 2 * oneFourth; // hooray, it works!
4. Operator*應該被實現為友元函數麼?
故事有了一個完美的結局,但是還有一個揮之不去的擔心。Operator*應該被實現為Rational類的友元麼?
在這種情況下,答案是No。因為operator*可以完全依靠Rational的public接口來實現。上面的代碼就是一種實現方式。我們能得到一個很重要的結論:成員函數的反義詞是非成員函數而不是友元函數。太多的c++程序員認為一個類中的函數如果不是一個成員函數(舉個例子,需要為所有參數做類型轉換),那麼他就應該是一個友元函數。上面的例子表明這樣的推理是有缺陷的。盡量避免使用友元函數,就像生活中的例子,朋友帶來的麻煩可能比從它們身上得到的幫助要多。
5. 其他問題如果你從面向對象C++轉換到template C++,將Rational實現成一個類模版,會有新的問題需要考慮,並且有新的方法來解決它們。這些問題,方法和設計參考Item 46。