# Item 24: 當類型轉換應該用于所有參數時,聲明為非成員函數
作者:Scott Meyers
譯者:fatalerror99 (iTePub's Nirvana)
發布:http://blog.csdn.net/fatalerror99/
在此書的 Introduction 中我談到讓一個類支持隱式類型轉換通常是一個不好的主意。當然,這條規則有一些例外,最普通的一種就是在創建數值類型時。例如,如果你設計一個用來表現有理數的類,允許從整數到有理數的隱式轉換看上去并非不合理。這的確不比 C++ 的內建類型從 int 到 double 的轉換更不合理(而且比 C++ 的內建類型從 double 到 int 的轉換合理得多)。在這種情況下,你可以用這種方法開始你的 Rational 類:
```
class Rational {
public:
Rational(int numerator = 0, // ctor is deliberately not explicit;
int denominator = 1); // allows implicit int-to-Rational
// conversions
int numerator() const; // accessors for numerator and
int denominator() const; // denominator — see Item 22
private:
...
};
```
你知道你應該支持算術運算,比如加法,乘法,等等,但是你不能確定是通過成員函數,非成員函數,還是非成員的友元函數來實現它們。你的直覺告訴你,當你搖擺不定的時候,你應該堅持面向對象的原則。你了解這一點,于是斷定,因為有理數的乘法與 Rational 類相關,所以在 Rational 類的內部實現有理數的 operator\* 似乎更加正常。但是,與直覺不符的是,Item 23 指出將函數放在它們所關聯的類的內部的主張有時候與面向對象的原則正好相反,但是讓我們將它先放到一邊,來研究一下讓 operator\* 成為 Rational 的一個成員函數的想法究竟如何:
```
class Rational {
public:
...
const Rational operator*(const Rational& rhs) const;
};
```
(如果你不能確定為什么這個函數聲明為這個樣子——返回一個 const by-value 的結果,卻持有一個 reference-to-const 作為它的參數——請參考 Item 3,20 和 21。)
這個設計讓你在有理數相乘時不費吹灰之力:
```
Rational oneEighth(1, 8);
Rational oneHalf(1, 2);
Rational result = oneHalf * oneEighth; // fine
result = result * oneEighth; // fine
```
但是你并不感到滿意。你還希望支持混合模式的操作,以便讓 Rationals 能夠和其它類型(例如,int)相乘。畢竟,很少有事情像兩個數相乘那么正常,即使它們碰巧是數字的不同類型。
當你試圖做混合模式的算術運算時,可是,你發現只有一半時間它能工作:
```
result = oneHalf * 2; // fine
result = 2 * oneHalf; // error!
```
這是一個不好的征兆。乘法必須是可交換的,記得嗎?
當你重寫最后兩個例子為功能等價的另一種形式時,問題的來源就變得很明顯了:
```
result = oneHalf.operator*(2); // fine
result = 2.operator*(oneHalf); // error!
```
對象 oneHalf 是一個包含 operator\* 的類的實例,所以編譯器調用那個函數。然而,整數 2 與類沒有關系,因而沒有 operator\* 成員函數。編譯器同樣要尋找能如下調用的非成員的 operator\*s(也就是說,在 namespace 或全局范圍內的 operator\*s):
```
result = operator*(2, oneHalf); // error!
```
但是在本例中,沒有非成員的持有一個 int 和一個 Rational 的 operator\*,所以搜索失敗。
再看一眼那個成功的調用。你會發現它的第二個參數是整數 2,然而 Rational::operator\* 卻持有一個 Rational 對象作為它的參數。這里發生了什么呢?為什么 2 在一個位置能工作,在其它地方卻不行呢?
發生的是隱式類型轉換。編譯器知道你傳遞一個 int 而那個函數需要一個 Rational,但是它們也知道通過用你提供的 int 調用 Rational 的構造函數,它們能做出一個相配的 Rational,這就是它們的所作所為。換句話說,它們將那個調用或多或少看成如下這樣:
```
const Rational temp(2); // create a temporary
// Rational object from 2
result = oneHalf * temp; // same as oneHalf.operator*(temp);
```
當然,編譯器這樣做僅僅是因為提供了一個非顯性的構造函數。如果 Rational 的構造函數是顯性的,這些語句都將無法編譯:
```
result = oneHalf * 2; // error! (with explicit ctor);
// can't convert 2 to Rational
result = 2 * oneHalf; // same error, same problem
```
支持混合模式操作失敗了,但是至少兩個語句的行為將步調一致。
然而,你的目標是既保持一致性又要支持混合運算,也就是說,一個能使上面兩個語句都可以編譯的設計。讓我們返回這兩個語句看一看,為什么即使 Rational 的構造函數不是顯式的,也是一個可以編譯而另一個不行:
```
result = oneHalf * 2; // fine (with non-explicit ctor)
result = 2 * oneHalf; // error! (even with non-explicit ctor)
```
其原因在于僅僅當參數列在參數列表中的時候,它們才有資格進行隱式類型轉換。而對應于成員函數被調用的那個對象的隱含參數—— this 指針指向的那個——根本沒有資格進行隱式轉換。這就是為什么第一個調用能編譯而第二個不能。第一種情況包括一個參數被列在參數列表中,而第二種情況沒有。
你還是希望支持混合運算,然而,現在做到這一點的方法或許很清楚了:讓 operator\* 作為非成員函數,因此就允許便一起將隱式類型轉換應用于所有參數:
```
class Rational {
... // contains no operator*
};
const Rational operator*(const Rational& lhs, // now a non-member
const Rational& rhs) // function
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
Rational oneFourth(1, 4);
Rational result;
result = oneFourth * 2; // fine
result = 2 * oneFourth; // hooray, it works!
```
這樣的確使故事有了一個圓滿的結局,但是有一個吹毛求疵的毛病。operator\* 應該不應該作為 Rational 類的友元呢?
在這種情況下,答案是不,因為 operator\* 能夠根據 Rational 的 public 接口完全實現。上面的代碼展示了做這件事的方法之一。這導出了一條重要的結論:與成員函數相對的是非成員函數,而不是友元函數。太多的程序員假設如果一個函數與一個類有關而又不應該作為成員時(例如,因為所有的參數都需要類型轉換),它應該作為友元。這個示例證明這樣的推理是有缺陷的。無論何時,只有你能避免友元函數,你就避免它,因為,就像在現實生活中,朋友的麻煩通常多于他們的價值。當然,有時友誼是正當的,但是事實表明僅僅因為函數不應該作為成員并不自動意味著它應該作為友元。
本 Item 包含真理,除了真理一無所有,但它還不是完整的真理。當你從 Object-Oriented C++ 穿過界線進入 Template C++(參見 Item 1)而且將 Rational 做成一個類模板代替一個類,就有新的問題要考慮,也有新的方法來解決它們,以及一些令人驚訝的設計含義。這樣的問題,解決方法和含義是 Item 46 的主題。
Things to Remember
* 如果你需要在一個函數的所有參數(包括被 this 指針所指向的那個)上使用類型轉換,這個函數必須是一個非成員。
- Preface(前言)
- Introduction(導言)
- Terminology(術語)
- Item 1: 將 C++ 視為 federation of languages(語言聯合體)
- Item 2: 用 consts, enums 和 inlines 取代 #defines
- Item 3: 只要可能就用 const
- Item 4: 確保 objects(對象)在使用前被初始化
- Item 5: 了解 C++ 為你偷偷地加上和調用了什么函數
- Item 6: 如果你不想使用 compiler-generated functions(編譯器生成函數),就明確拒絕
- Item 7: 在 polymorphic base classes(多態基類)中將 destructors(析構函數)聲明為 virtual(虛擬)
- Item 8: 防止因為 exceptions(異常)而離開 destructors(析構函數)
- Item 9: 絕不要在 construction(構造)或 destruction(析構)期間調用 virtual functions(虛擬函數)
- Item 10: 讓 assignment operators(賦值運算符)返回一個 reference to *this(引向 *this 的引用)
- Item 11: 在 operator= 中處理 assignment to self(自賦值)
- Item 12: 拷貝一個對象的所有組成部分
- Item 13: 使用對象管理資源
- Item 14: 謹慎考慮資源管理類的拷貝行為
- Item 15: 在資源管理類中準備訪問裸資源(raw resources)
- Item 16: 使用相同形式的 new 和 delete
- Item 17: 在一個獨立的語句中將 new 出來的對象存入智能指針
- Item 18: 使接口易于正確使用,而難以錯誤使用
- Item 19: 視類設計為類型設計
- Item 20: 用 pass-by-reference-to-const(傳引用給 const)取代 pass-by-value(傳值)
- Item 21: 當你必須返回一個對象時不要試圖返回一個引用
- Item 22: 將數據成員聲明為 private
- Item 23: 用非成員非友元函數取代成員函數
- Item 24: 當類型轉換應該用于所有參數時,聲明為非成員函數
- Item 25: 考慮支持不拋異常的 swap
- Item 26: 只要有可能就推遲變量定義
- Item 27: 將強制轉型減到最少
- Item 28: 避免返回對象內部構件的“句柄”
- Item 29: 爭取異常安全(exception-safe)的代碼
- Item 30: 理解 inline 化的介入和排除
- Item 31: 最小化文件之間的編譯依賴
- Item 32: 確保 public inheritance 模擬 "is-a"
- Item 33: 避免覆蓋(hiding)“通過繼承得到的名字”
- Item 34: 區分 inheritance of interface(接口繼承)和 inheritance of implementation(實現繼承)
- Item 35: 考慮可選的 virtual functions(虛擬函數)的替代方法
- Item 36: 絕不要重定義一個 inherited non-virtual function(通過繼承得到的非虛擬函數)
- Item 37: 絕不要重定義一個函數的 inherited default parameter value(通過繼承得到的缺省參數值)
- Item 38: 通過 composition(復合)模擬 "has-a"(有一個)或 "is-implemented-in-terms-of"(是根據……實現的)
- Item 39: 謹慎使用 private inheritance(私有繼承)
- Item 40: 謹慎使用 multiple inheritance(多繼承)
- Item 41: 理解 implicit interfaces(隱式接口)和 compile-time polymorphism(編譯期多態)
- Item 42: 理解 typename 的兩個含義
- Item 43: 了解如何訪問 templatized base classes(模板化基類)中的名字
- Item 44: 從 templates(模板)中分離出 parameter-independent(參數無關)的代碼
- Item 45: 用 member function templates(成員函數模板) 接受 "all compatible types"(“所有兼容類型”)
- Item 46: 需要 type conversions(類型轉換)時在 templates(模板)內定義 non-member functions(非成員函數)
- Item 47: 為類型信息使用 traits classes(特征類)
- Item 48: 感受 template metaprogramming(模板元編程)
- Item 49: 了解 new-handler 的行為
- Item 50: 領會何時替換 new 和 delete 才有意義
- Item 51: 編寫 new 和 delete 時要遵守慣例
- Item 52: 如果編寫了 placement new,就要編寫 placement delete
- 附錄 A. 超越 Effective C++
- 附錄 B. 第二和第三版之間的 Item 映射