Item 46: Define non-member functions inside templates when type conversions are desired.
Item 24中提到,如果所有參數都需要隱式類型轉換,該函數應當聲明為非成員函數。Item 24是以Rational
和operator*
為例子展開的,本文把這個觀點推廣到類模板和函數模板。 但是在類模板中,需要所有參數隱式轉換的函數應當聲明為友元並定義在類模板中。
既然是Item 24的推廣,那麼我們先把Item24中的Rational
和operator*
模板化。得到如下代碼:
template
class Rational {
public:
Rational(const T& numerator = 0, const T& denominator = 1);
const T numerator() const;
const T denominator() const;
};
template
const Rational operator*(const Rational& lhs, const Rational& rhs){}
Item 20解釋了為什麼
Rational
的參數是常量引用;Item 28解釋了為什麼numerator()
返回的是值而不是引用;Item 3解釋了為什麼numerator()
返回的是const
。
上述代碼是Item24直接模板化的結果,看起來很完美但它是有問題的。比如我們有如下的調用:
Rational oneHalf(1, 2); // OK
Rational result = oneHalf * 2; // Error!
為什麼第二條會出錯呢?因為編譯器無法推導出合適的模板參數來實例化Rational
。 模板參數的推導包括兩部分:
onHalf
,它的類型是Rational
,很容易知道接受oneHalf
的operator*
中模板參數T
應該是int
;2
的模板參數推導卻不那麼順利,編譯器不知道如何將實例化operator*
才能使得它接受一個int
類型的2
。
可能你會希望編譯器將2
的類型推導為Rational
,再進行隱式轉換。但在編譯器中模板推導和函數調用是兩個過程: 隱式類型轉換發生在函數調用時,而在函數調用之前編譯器需要實例化一個函數。而在模板實例化的過程中,編譯器無從推導T
的類型。
為了讓編譯器知道T
是什麼,我們可以在類模板中通過friend
聲明來引用一個外部函數。
template
class Rational {
public:
friend const Rational operator*(const Rational& lhs, const Rational& rhs);
};
template
const Rational operator*(const Rational& lhs, const Rational& rhs){}
在
Rational
中聲明的friend
沒有添加模板參數T
,這是一個簡便寫法,它完全等價於:friend const Rational
。operator*(const Rational & lhs, const Rational & rhs);
因為類模板實例化後,T
總是已知的,因而那個friend
函數的簽名會被Rational
模板類聲明。 這樣,result = oneHalf * 2
便可以編譯通過了,但鏈接會出錯。 雖然在類中聲明了friend operator*
,然而編譯器卻不會實例化該聲明對應的函數。 因為函數是我們自己聲明的,那麼編譯器認為我們有義務自己去定義那個函數。
那我們就在聲明operator*
時直接給出定義:
template
class Rational {
public:
friend const Rational operator*(const Rational& lhs, const Rational& rhs){
return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
};
這樣混合模式的調用result = oneHalf * 2
終於可以編譯、鏈接並且運行了。到這裡想必問題已經很清楚了:
operator*
需要聲明為非成員函數;operator*
需要在類中聲明;friend
;friend
聲明中。
雖然operator*
可以成功運行了,但定義在類定義中的函數是inline函數,見Item 30。 如果operator*
函數體變得很大,那麼inline函數就不再合適了,這時我們可以讓operator*
調用外部的一個輔助函數:
template class Rational;
template
const Rational doMultiply(const Rational& lhs, const Rational& rhs);
template
class Rational{
public:
friend Rational operator*(const Rational& lhs, const Rational& rhs){
return doMultiply(lhs, rhs);
}
};
doMultiply
仍然是不支持混合模式調用的,然而doMultiply
只會被operator*
調用。operator*
將會完成混合模式的兼容,然後用統一的Rational
類型參數來調用doMultiply
。