# Item 46: 需要 type conversions(類型轉換)時在 templates(模板)內定義 non-member functions(非成員函數)
作者:Scott Meyers
譯者:fatalerror99 (iTePub's Nirvana)
發布:http://blog.csdn.net/fatalerror99/
Item 24 闡述了為什么只有 non-member functions(非成員函數)適合于應用到所有 arguments(實參)的 implicit type conversions(隱式類型轉換),而且它還作為一個示例使用了一個 Rational class 的 operator\* function。我建議你在繼續下去之前先熟悉那個示例,因為這個 Item 進行了針對 Item 24 中的示例的一個表面上的無傷大雅的更改(模板化 Rational 和 operator\*)的擴展討論:
```
template<typename T>
class Rational {
public:
Rational(const T& numerator = 0, // see Item 20 for why params
const T& denominator = 1); // are now passed by reference
const T numerator() const; // see Item 28 for why return
const T denominator() const; // values are still passed by value,
... // Item 3 for why they're const
};
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs,
const Rational<T>& rhs)
{ ... }
```
就像在 Item 24 中,我想要支持 mixed-mode arithmetic(混合模式運算),所以我們要讓下面這些代碼能夠編譯。我們指望它能,因為我們使用了和 Item 24 中可以工作的代碼相同的代碼。僅有的區別是 Rational 和 operator\* 現在是 templates(模板):
```
Rational<int> oneHalf(1, 2); // this example is from Item 24,
// except Rational is now a template
Rational<int> result = oneHalf * 2; // error! won't compile
```
編譯失敗的事實暗示對于模板化 Rational 來說,有某些東西和 non-template(非模板)版本不同,而且確實存在。在 Item 24 中,編譯器知道我們想要調用什么函數(取得兩個 Rationals 的 operator\*),但是在這里,編譯器不知道我們想要調用哪個函數。作為替代,它們試圖 figure out(推斷)要從名為 operator\* 的 template(模板)中實例化出(也就是創建)什么函數。它們知道它們假定實例化出的某個名為 operator\* 的函數取得兩個 Rational<T> 類型的參數,但是為了做這個實例化,它們必須 figure out(推斷)T 是什么。問題在于,它們做不到。
在推演 T 的嘗試中,它們會察看被傳入 operator\* 的調用的 arguments(實參)的類型。在當前情況下,類型為 Rational<int>(oneHalf 的類型)和 int(2 的類型)。每一個參數被分別考察。
使用 oneHalf 的推演很簡單。operator\* 的第一個 parameter(形參)被聲明為 Rational<T> 類型,而傳入 operator\* 的第一個 argument(實參)(oneHalf) 是 Rational<int> 類型,所以 T 一定是 int。不幸的是,對其它參數的推演沒那么簡單。operator\* 的第二個 parameter(形參)被聲明為 Rational<T> 類型,但是傳入 operator\* 的第二個 argument(實參)(2) 的 int 類型。在這種情況下,讓編譯器如何 figure out(推斷)T 是什么呢?你可能期望它們會使用 Rational<int> 的 non-explicit constructor(非顯式構造函數)將 2 轉換成一個 Rational<int>,這樣就使它們推演出 T 是 int,但是它們不這樣做。它們不這樣做是因為在 template argument deduction(模板實參推演)過程中從不考慮 implicit type conversion functions(隱式類型轉換函數)。從不。這樣的轉換可用于函數調用過程,這沒錯,但是在你可以調用一個函數之前,你必須知道哪個函數存在。為了知道這些,你必須為相關的 function templates(函數模板)推演出 parameter types(參數類型)(以便你可以實例化出合適的函數)。但是在 template argument deduction(模板實參推演)過程中不考慮經由 constructor(構造函數)調用的 implicit type conversion(隱式類型轉換)。Item 24 不包括 templates(模板),所以 template argument deduction(模板實參推演)不是一個問題,現在我們在 C++ 的 template 部分(參見 Item 1),這是主要問題。
在一個 template class(模板類)中的一個 friend declaration(友元聲明)可以指涉到一個特定的函數,我們可以利用這一事實為受到 template argument deduction(模板實參推演)挑戰的編譯器解圍。這就意味著 class Rational<T> 可以為 Rational<T> 聲明作為一個 friend function(友元函數)的 operator\*。class templates(類模板)不依靠 template argument deduction(模板實參推演)(這個過程僅適用于 function templates(函數模板)),所以 T 在 class Rational<T> 被實例化時總是已知的。通過將適當的 operator\* 聲明為 Rational<T> class 的一個 friend(友元)使其變得容易:
```
template<typename T>
class Rational {
public:
...
friend // declare operator*
const Rational operator*(const Rational& lhs, // function (see
const Rational& rhs); // below for details)
};
template<typename T> // define operator*
const Rational<T> operator*(const Rational<T>& lhs, // functions
const Rational<T>& rhs)
{ ... }
```
現在我們對 operator\* 的混合模式調用可以編譯了,因為當 object oneHalf 被聲明為 Rational<int> 類型時,class Rational<int> 被實例化,而作為這一過程的一部分,取得 Rational<int> parameters(形參)的 friend function(友元函數)operator\* 被自動聲明。作為已聲明 function(函數)(并非一個 function template(函數模板)),在調用它的時候編譯器可以使用 implicit conversion functions(隱式轉換函數)(譬如 Rational 的 non-explicit constructor(非顯式構造函數)),而這就是它們如何使得混合模式調用成功的。
唉,在這里的上下文中,“成功”是一個可笑的詞,因為盡管代碼可以編譯,但是不能連接。但是我們過一會兒再處理它,首先我想討論一下用于在 Rational 內聲明 operator\* 的語法。
在一個 class template(類模板)內部,template(模板)的名字可以被用做 template(模板)和它的 parameters(參數)的縮寫,所以,在 Rational<T> 內部,我們可以只寫 Rational 代替 Rational<T>。在本例中這只為我們節省了幾個字符,但是當有多個參數或有更長的參數名時,這既能節省擊鍵次數又能使最終的代碼顯得更清晰。我把這一點提前,是因為 operator\* 被聲明為取得并返回 Rationals,而不是 Rational<T>s。它就像如下這樣聲明 operator\* 一樣合法:
```
template<typename T>
class Rational {
public:
...
friend
const Rational<T> operator*(const Rational<T>& lhs,
const Rational<T>& rhs);
...
};
```
然而,使用縮寫形式更簡單(而且更常用)。
現在返回到連接問題。混合模式代碼編譯,因為編譯器知道我們想要調用一個特定的函數(取得一個 Rational<int> 和一個 Rational<int> 的 operator\*),但是那個函數只是在 Rational 內部 declared(被聲明),而沒有在此處 defined(被定義)。我們打算讓 class 之外的 operator\* template(模板)提供這個定義,但是這種方法不能工作。如果我們自己聲明一個函數(這就是我們在 Rational template(模板)內部所做的事),我們就有責任定義這個函數。當前情況是,我們沒有提供定義,這也就是連接器為什么不能找到它。
讓它能工作的最簡單的方法或許就是將 operator\* 的本體合并到它的 declaration(定義)中:
```
template<typename T>
class Rational {
public:
...
friend const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(), // same impl
lhs.denominator() * rhs.denominator()); // as in
} // Item 24
};
```
確實,這樣就可以符合預期地工作:對 operator\* 的混合模式調用現在可以編譯,連接,并運行。萬歲!
關于此技術的一個有趣的觀察結論是 friendship 的使用對于訪問 class 的 non-public parts(非公有構件)的需求并沒有起到什么作用。為了讓所有 arguments(實參)的 type conversions(類型轉換)成為可能,我們需要一個 non-member function(非成員函數)(Item 24 依然適用);而為了能自動實例化出適當的函數,我們需要在 class 內部聲明這個函數。在一個 class 內部聲明一個 non-member function(非成員函數)的唯一方法就是把它做成一個 friend(友元)。那么這就是我們做的。反傳統嗎?是的。有效嗎?毫無疑問。
就像 Item 30 闡述的,定義在一個 class 內部的函數被隱式地聲明為 inline(內聯),而這也包括像 operator\* 這樣的 friend functions(友元函數)。你可以讓 operator\* 不做什么事情,只是調用一個定義在這個 class 之外的 helper function(輔助函數),從而讓這樣的 inline declarations(內聯聲明)的影響最小化。在本 Item 的這個示例中,沒有特別指出這樣做,因為 operator\* 已經可以實現為一個 one-line function(單行函數),但是對于更復雜的函數體,這樣做也許是合適的。"have the friend call a helper"(“讓友元調用輔助函數”)的方法還是值得注意一下的。
Rational 是一個 template(模板)的事實意味著那個 helper function(輔助函數)通常也是一個 template(模板),所以典型情況下在頭文件中定義 Rational 的代碼看起來大致如下:
```
template<typename T> class Rational; // declare
// Rational
// template
template<typename T> // declare
const Rational<T> doMultiply(const Rational<T>& lhs, // helper
const Rational<T>& rhs); // template
template<typename T>
class Rational {
public:
...
friend
const Rational<T> operator*(const Rational<T>& lhs,
const Rational<T>& rhs) // Have friend
{ return doMultiply(lhs, rhs); } // call helper
...
};
```
多數編譯器基本上會強迫你把所有的 template definitions(模板定義)都放在頭文件中,所以你可能同樣需要在你的頭文件中定義 doMultiply。(就像 Item 30 闡述的,這樣的 templates(模板)不需要 inline(內聯)。)可能看起來就像這樣:
```
emplate<typename T> // define
const Rational<T> doMultiply(const Rational<T>& lhs, // helper
const Rational<T>& rhs) // template in
{ // header file,
return Rational<T>(lhs.numerator() * rhs.numerator(), // if necessary
lhs.denominator() * rhs.denominator());
}
```
當然,作為一個 template(模板),doMultiply 不支持混合模式乘法,但是它不需要。它只被 operator\* 調用,而 operator\* 支持混合模式運算!本質上,function operator\* 支持為了確保被相乘的是兩個 Rational objects 而必需的各種 type conversions(類型轉換),然后它將這兩個 objects 傳遞給一個 doMultiply template(模板)的適當的實例化來做實際的乘法。配合行動,不是嗎?
Things to Remember
* 在寫一個 class template(類模板),而這個 class template(類模板)提供了一些 函數,這些函數指涉到支持所有 parameters(參數)的 implicit type conversions(隱式類型轉換)的 template(模板)的時候,把這些函數定義為 class template(類模板)內部的 friends(友元)。
- 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 映射