條款24說過為什麼惟有non-member函數才有能力“在所有實參身上實施隱式類型轉換”。本條款將Rational和operator*模板化:
template<typename T>
class Rational{
Rational(const T& number = 0,
const T& denominator = 1);
const T number() const;
const T denominator() const;
};
template<typename T>
const Rational<T> operator* (const Rational<T>& lhs, const Rational<T>& rhs)
{......}
像條款24一樣,我們希望支援混合算術運算,我們希望一下代碼順利通過編譯:
Rational<int> oneHalf(1,2);
Rational<int> result = oneHalf * 2;//錯誤!無法通過編譯
條款24內,編譯器知道我們嘗試調用什麼函數(就是接受兩個Rationals參數的那個operator*),但這裡編譯器不知道我們想要調用哪個函數。取而代之的是,他們試圖想出什麼函數被名為operator*的template具現化(產生)出來。它們知道應該可以具現化某個“名為operator*並接受兩個Rational<T>參數”的函數,但完成這一具現化行動,必須先算出T是什麼。問題是它們沒這個能耐。
為了推導T,它們看了看operator*調用動作中的參數類型。每個參數分開考慮。
以oneHalf進行推導,過程並不困難。T一定是int。第二個參數被聲明是Rational<T>,但傳遞給operator*的第二個實參(2)類型是int。編譯器如何根據這個推算出T?你或許會期盼編譯器使用Rational<int>的non-explicit建構函式將2轉換成Rational<int>,進而將T推導成int。但它們不那麼做,因為在template實參推導過程中從不將隱式類型轉換函式納入考慮。絕不!這樣的轉換在函數調用過程中的卻被使用,但在能夠調用一個函數之前,首先必須知道那個函數存在。為了知道它,必須先為相關的function template推匯出參數類型(然後才可將適當的函數具現化出來)。template實參推導過程中並不考慮採納“通過建構函式而發生的”隱式類型轉換。
有方法可以緩和編譯器在template實參推導方面受到的挑戰:template class內的friend聲明式可以指涉某個特定函數。
class template並不依賴template實參推導(後者只施行於function template身上),所以編譯器總是能在class Rational<T>具現化時得知T,所以令Rational<T> class 聲明適當的operator*為其friend函數,可簡化整個問題:
template<typename T>
class Rational{
public:
friend
const Rational operator* (const Rational& lhs, const Rational& rhs); //聲明operator*函數
};
template<typename T>
const Rational<T> operator* (const Rational<T>& lhs, const Rational<T>& rhs)
{......}
現在對operator*的混合式調用可以通過編譯了。因為當對象oneHalf被聲明為一個Rational<int>,class Rational<int>於是被具現化出來,而作為過程的一部分,friends函數operator*(接受Rational<int>參數)也就被自動聲明出來。後者身為一個函數而非函數模板,因此編譯器可在調用它的時候使用隱式轉換函式(例如Rational的non-explicit建構函式)。
在一個class template中,template名稱可被用來作為“template和其參數”的簡略表達方式,所以在Rational<T>內,我們可以唯寫Rational而不必寫Ratioan<T>。和如下聲明一樣:
template<typename T>
class Rational{
public:
friend
const Rational<T> operator* (const Rational<T>& lhs, const Rational<T>& rhs); //聲明operator*函數
};
然而使用簡略表達方式(速記式)比較輕鬆也比較普遍。
這個函數只被聲明於Rational內,並沒有被定義出來。我們意圖令此class外部的operator* template提供定義式,但是行不通。我們在class Rational template內聲明了一個函數,就有責任定義那個函數。
或許最簡單的可行方法就是將operator*函數本體合并至其聲明式內:
template<typename T>
class Rational{
public:
friend
const Rational operator* (const Rational& lhs, const Rational& rhs); //聲明operator*函數
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
};
我們雖然使用friend,卻與friend的傳統用途“訪問class的non-public成分”毫不相干。為了讓類型轉換可能發生在所有參數身上,我們需要一個non-member函數:為了令這個函數被自動具現化,我們需要將它聲明在一個class內部:而在class內部聲明non-member函數的唯一辦法就是:令他成為一個friend。
定義於class內的函數都暗自成為inline,包括operator*這樣的friend函數。你可以將這樣的inline聲明所帶來的衝擊最小化,做法是令operator*不做任何事情,只調用一個定義於class外部的輔助函數。Rational是個template意味著這個輔助函數通常也是一個template。Rational標頭檔代碼,很典型的長這個樣子:
template<typename T> class Rational; //聲明Rational template
template<typename T>
const Rational<T> doMultiply (const Rational<T>& lhs, const Rational<T>& rhs); //聲明helper template
template<typename T>
class Rational{
public:
...
friend
const Rational<T> operator* (const Rational<T>& lhs, const Rational<T>& rhs);
{
return doMultiply(lhs, rhs); //令friend調用helper
}
};
許多編譯器實質上會強迫你把所有template定義式放進標頭檔內,所以你或許需要在標頭檔內定義doMultiply(條款30,這樣的templates不需非得是inline不可),看起來像這樣:
template<typename T>
const Rational<T> doMultiply (const Rational<T>& lhs, const Rational<T>& rhs)
{
return Rational<T>(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
作為一個template,doMultiply當然不支援混合式乘法,但它其實也不需要。它只被operator*調用,而operator*支援混合式操作!