讀書筆記_Effective_C++_條款四十六:須要類型轉換時請爲模板定義非成員函數

這個條款能夠當作是條款24的續集,咱們先簡單回顧一下條款24,它說了爲何相似於operator *這樣的重載運算符要定義成非成員函數(是爲了保證混合乘法2*SomeRational或者SomeRational*2均可以經過編譯,2不能同時進行隱式類型轉換成某個Rational,再做this用)。函數

因此咱們通常將之定義成友元函數,像下面這樣:this

 1 class Rational
 2 {
 3 private:
 4     int numerator;
 5     int denominator;
 6 public:
 7     Rational(int n = 0, int d = 1): numerator(n), denominator(d){assert(denominator != 0);}
 8     int GetNumerator() const{return numerator;}
 9     int GetDenominator() const {return denominator;}
10     friend const Rational operator* (const Rational& r1, const Rational& r2);
11 };
12 const Rational operator* (const Rational& r1, const Rational& r2)
13 {
14     return Rational(r1.numerator * r2.numerator, r1.denominator * r2.denominator);
15 }

如今咱們來引入模板,能夠像下面這樣寫,注意這裏的operator*是一個獨立的模板函數:spa

 1 template <class T>
 2 class Rational
 3 {
 4 private:
 5     T Numerator;
 6     T Denominator;
 7 
 8 public:
 9     Rational(const T& Num = 0, const T& Den = 1) : Numerator(Num), Denominator(Den){}
10     const T GetNumerator() const
11     {
12         return Numerator;
13     }
14 
15     const T GetDenominator() const
16     {
17         return Denominator;
18     }
19 
20     string ToString() const
21     {
22         stringstream ss;
23         ss << Numerator << "/" << Denominator;
24         return ss.str();
25     }
26 };
27 
28 template <class T>
29 const Rational<T> operator* (const Rational<T>& a, const Rational<T>& b)
30 {
31     return Rational<T>(a.GetNumerator() * b.GetNumerator(), 
32         a.GetDenominator() * b.GetDenominator() );
33 }

但下面main函數的兩行卻都不能經過編譯:code

1 int main()
2 {
3     Rational<int> a(3, 5);
4     Rational<int> c = a * 2; // 不能經過編譯!
5     c = 2 * a;               // 不能經過編譯!
6     cout << c.ToString() << endl;
7 }

緣由是編譯器推導T出現了困難,a * 2在編譯器看來,能夠由a是Rational<int>將T推導成int,可是2是什麼,理想狀況下編譯器會嘗試將它先轉換成一個Rational<int>,並將T推導成int,但事實上編譯器在「T推導過程當中從不將隱式類型轉換函數歸入考慮」。因此不管是a * 2仍是2 * a都是不能經過編譯的,一句話,隱式轉換+推導T不能被同時被編譯器接受。blog

解決問題的思路便接着產生,編譯器既然不能同時接受這兩個過程,就讓它們事先知足好一個條件,再由編譯器執行另外一個過程好了。ip

若是把這個operator*放在template class裏面,也就是先在生成模板類的那一步就定下T,這樣編譯器只要執行隱式轉換這一步就能夠了。編譯器

所以咱們能夠這樣來改:string

 1 template <class T>
 2 class Rational
 3 {
 4  5     friend Rational operator* (const Rational& a, const Rational& b);  6 };
 7 
 8 template <class T>
 9 const Rational<T> operator* (const Rational<T>& a, const Rational<T>& b)
10 {
11     // 這裏友元函數的聲明並非用來訪問類的私有成員的,而是用來進行事先類型推導的
12     return Rational<T>(a.GetNumerator() * b.GetNumerator(), 
13         a.GetDenominator() * b.GetDenominator() );
14 }

注意紅色部分,咱們添加了一個友元函數的聲明,果真編譯經過了,但連接時又報錯了,緣由是連接器找不到operator*的定義,這裏又要說模板類中的一個特殊狀況了,它不一樣與普通的類,模板類的友元函數只能在類中實現,因此要把函數體部分移至到類內,像下面這樣:io

 1 template <class T>
 2 class Rational
 3 {
 4  5     friend Rational operator* (const Rational& a, const Rational& b)
 6     {
 7         return Rational (a.GetNumerator() * b.GetNumerator(),
 8             a.GetDenominator() * b.GetDenominator());
 9     }
10 11 }

這下編譯和連接都沒有問題了。這裏還要說一下,就是移至類內後,T的標識符能夠不寫了,但若是非要寫成下面這樣,天然也是OK的。編譯

1 friend Rational<T> operator* (const Rational<T>& a, const Rational<T>& b)
2 {
3     return Rational<T>(a.GetNumerator() * b.GetNumerator(),
4         a.GetDenominator() * b.GetDenominator());
5 }

operator*裏面只有一句話,但若是friend函數裏面的東西太多了,能夠定義一個輔助方法,好比DoMultiply(),這個DoMultiply能夠放在類外去實現,DoMultiply自己不支持混合乘法(2 * SomeRational或者SomeRational * 2),但因爲在operator*裏面已經進行了隱式類型轉換,因此到DoMultiply這一級是沒有問題的。

 

最後總結一下:

當咱們編寫一個class template,而它所提供之「與此template相關的」函數支持「全部參數之隱式類型轉換」時,請將那些函數定義爲「class template內部的friend函數」。

相關文章
相關標籤/搜索