Item 24中解釋了為什麼對於所有參數的隱式類型轉換,只有非成員函數是合格的,並且使用了一個為Rational 類創建的operator*函數作為實例。在繼續之前建議你先回顧一下這個例子,因為這個條款的討論是對它的擴展,我們會對Item 24的實例做一些看上去無傷大雅的修改:對Rational和opeartor*同時進行模板化:
1 template<typename T> 2 class Rational { 3 public: 4 Rational(const T& numerator = 0, // see Item 20 for why params 5 6 const T& denominator = 1); // are now passed by reference 7 8 const T numerator() const; // see Item 28 for why return 9 10 11 const T denominator() const; // values are still passed by value, 12 ... // Item 3 for why they’re const 13 }; 14 template<typename T> 15 const Rational<T> operator*(const Rational<T>& lhs, 16 const Rational<T>& rhs) 17 { ... }
正如Item 24中討論的,我們想支持混合模式的算術運算,所以我們想讓下面的代碼通過編譯。這應該沒有問題,因為我們在Item 24中使用了相同的代碼。唯一的區別是Rational和operator*現在變成了模板:
1 Rational<int> oneHalf(1, 2); // this example is from Item 24, 2 // except Rational is now a template 3 4 Rational<int> result = oneHalf * 2; // error! won’t compile
但事實上上面的代碼不會通過編譯,這就表明了模板化的Rational和非模板版本有些地方還是不一樣的,確實是有區別的。在Item
24中,編譯器知道我們嘗試調用什麼函數(帶兩個Rational參數的operator*),但是這裡,編譯器不知道我們想要調用哪個函數。相反,它們嘗試確認從模板operator*中實例化出(也即是創建)什麼樣的函數。它們知道它們想實例化一些名字為operator*的函數,這些函數帶有兩個類型為Rational<T>的參數,但是為了進行實例化,它們必須確認T是什麼。問題是他們不可能知道。
為了演繹出T類型,它們看到了調用operator*時傳遞的參數類型。在上面的例子中,兩個參數類型分別是Rational<int>(oneHalf的類型)和int(2的類型)。每個參數進行單獨考慮。
使用oneHalf進行演繹(deduction)很容易,operator*的第一個參數所需要的類型為Rational<T>,實際上這裡傳遞給operator*的第一個參數的類型是Rational<int>,所以T必須為int。不幸的是,對其他參數的演繹就沒有這麼簡單了,operator*的第二個參數所需要的類型也為Rational<T>,但是傳遞給operator*的第二個參數是一個int值。在這種情況下編譯器該如何確認T是什麼呢?你可能期望它們使用Rational<int>的非顯示構造函數來將2轉換為一個Rational<int>,這樣就允許它們將T演繹成int,但是它們沒有這麼做。因為在模板參數演繹期間永遠不會考慮使用隱式類型轉換函數。這樣的轉換是在函數調用期間被使用的,所以在你調用一個函數之前,你必須知道哪個函數是存在的。為了知道這些,你就必須為相關的函數模板演繹出參數類型(然後你才能實例化出合適的函數。)但是在模板參數演繹期間不會通過調用構造函數來進行隱式轉換。Item 24沒有涉及到模板,所以模板參數的演繹不是問題。現在我們正在討論C++的模板部分(Item 1),這變為了主要問題。
我們可以利用如下事實來緩和編譯器接受的對模板參數演繹的挑戰:模板類中的一個友元聲明能夠引用一個實例化函數。這就意味著類Ration<T>能夠為Ration<T>聲明一個友元函數的operator*。類模板不再依賴於模板參數演繹(這個過程只應用於函數模板),所以T總是在類Ration<T>被實例化的時候就能被確認。所以聲明一個合適的友元operator*函數能簡化整個問題:
1 template<typename T> 2 class Rational { 3 public: 4 ... 5 6 friend // declare operator* 7 const Rational operator*(const Rational& lhs, // function (see 8 const Rational& rhs); // below for details) 9 }; 10 11 template<typename T> // define operator* 12 13 const Rational<T> operator*(const Rational<T>& lhs, // functions 14 15 16 const Rational<T>& rhs) 17 { ... }
現在我們對operator*的混合模式的調用就能通過編譯了,因為當對象oneHalf被聲明為類型Rational<int>的時候,Ratinonal<T>被實例化稱Rational<int>,作為這個過程的一部分,參數為Rational<int>的友元函數operator*被自動聲明。作為一個被聲明的函數(不是函數模板),編譯器在調用它時就能夠使用隱式類型轉換函數(像Rational的非顯示構造函數),這就是如何使混合模式調用成功的。
雖然代碼能夠通過編譯,但是卻不能鏈接成功。我們稍後處理,但是對於上面的語法我首先要討論的是在Rational中聲明operator*。
在一個類模板中,模板的名字能夠被用來當作模板和模板參數的速寫符號,所以在Rational<T>中,我們可以寫成Rational來代替Rational<T>。在這個例子中只為我們的輸入減少了幾個字符,但是如果有多個參數或者更長的參數名字的時候,它既能減少輸入也能使代碼看起來更清晰。我提出這些是因為在上面的例子中operator*的聲明用Rational作為參數和返回值,而不是Rational<T>。下面的聲明效果是一樣的:
1 template<typename T> 2 class Rational { 3 public: 4 ... 5 friend 6 const Rational<T> operator*(const Rational<T>& lhs, 7 const Rational<T>& rhs); 8 ... 9 };
然而,使用速寫形式更加容易(更加大眾化)。
現在讓我們回到鏈接問題。混合模式的代碼能夠通過編譯,因為編譯器知道我們想調用一個實例化函數(帶兩個Rational<int>參數的operator*函數),但是這個函數只在Rational內部進行聲明,而不是被定義。我們的意圖是讓類外部的operator*模板提供定義,但是編譯器不會以這種方式進行工作。如果我們自己聲明一個函數(這是我們在Rational模板內部所做的),我們同樣有責任去定義這個函數。在上面的例子中,我們並沒有提供一個定義,這就是為什麼連接器不能知道函數定義的原因。
最簡單的可能工作的解決方案是將operator*函數體合並到聲明中:
1 template<typename T> 2 class Rational { 3 public: 4 ... 5 friend const Rational operator*(const Rational& lhs, const Rational& rhs) 6 { 7 return Rational(lhs.numerator() * rhs.numerator(), // same impl 8 lhs.denominator() * rhs.denominator()); // as in 9 } // Item 24 10 }; 11 12
確實這能夠工作:對operator*的混合模式調用,編譯,鏈接,運行都沒有問題。
這種技術的有意思的地方是友元函數並沒有被用來訪問類的非public部分。為了使所有參數間的類型轉換成為可能,我們需要一個非成員函數(Item 24在這裡仍然適用);並且為了讓合適的函數被自動實例化出來,我們需要在類內部聲明一個函數。在類內部聲明一個非成員函數的唯一方法是將其聲明為友元函數。這就是我們所做的,不符合慣例?是的。有效麼?毋庸置疑。
正如在Item 30中解釋的,在類內部定義的函數被隱式的聲明為inline函數,這同樣包含像operator*這樣的友元函數。你可以最小化這種inline聲明的影響:通過讓operator*只調用一個定義在類體外的helper函數。在這個條款的例子中沒有必要這麼做,因為operator*已經被實現成了只有一行的函數,對於更加復雜的函數體,helper才可能是你想要的。“讓友元函數調用helper”的方法值得一看。
Rationl是模板的事實意味著helper函數通常情況下也會是一個模板,所以在頭文件中定義Rational的代碼會像下面這個樣子:
1 template<typename T> class Rational; // declare 2 // Rational 3 // template 4 5 template<typename T> // declare 6 const Rational<T> doMultiply( const Rational<T>& lhs, // helper 7 8 const Rational<T>& rhs); // template 9 10 template<typename T> 11 class Rational { 12 public: 13 ... 14 friend 15 const Rational<T> operator*(const Rational<T>& lhs, 16 const Rational<T>& rhs) // Have friend 17 18 19 20 { return doMultiply(lhs, rhs); } // call helper 21 22 ... 23 24 };
許多編譯器從根本上強制你將所有的模板定義放在頭文件中,所以你可能同樣需要在你的頭文件中定義doMultiply。(正如Item30解釋的,這樣的模板不需要inline)。這會像下面這個樣子:
template<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()); }
當然,作為一個模板,doMultiply不支持混合模式的乘法,但是也不需要支持。它只被operator*調用,operator*支持混合模式操作就夠了!從根本上來說,函數operator*支持必要的類型轉換,以確保兩個Rational對象被相乘,然後它將這兩個對象傳遞到doMultiply模板的合適實例中進行實際的乘法操作。協同行動,不是麼?
當實現一個提供函數的類模版時,如果這些函數的所有參數支持和模板相關的隱式類型轉換,將這些函數定義為類模板內部的友元函數。