C++型別轉換時定義非成員函式(轉)

ba發表於2007-08-15
C++型別轉換時定義非成員函式(轉)[@more@]《C++箴言:宣告為非成員函式的時機》闡述了為什麼只有 non-member functions(非成員函式)適合於應用到所有 arguments(實參)的 implicit type conversions(隱式型別轉換),而且它還作為一個示例使用了一個 Rational class 的 operator* function。我建議你在閱讀本文之前先熟悉那個示例,因為本文進行了針對《C++箴言:宣告為非成員函式的時機》中的示例做了一個無傷大雅(模板化 Rational 和 operator*)的擴充套件討論:

template
class Rational {
public:
Rational(const T& numerator = 0, // see《C++箴言:用傳引用給const取代傳值》for why params
const T& denominator = 1); // are now passed by reference

const T numerator() const; // see《C++箴言:避免返回物件內部構件的控制程式碼》for why return
const T denominator() const; // values are still passed by value,
... // Item 3 for why they're const
};

template
const Rational operator*(const Rational& lhs,
const Rational& rhs)
{ ... }

就像在《C++箴言:宣告為非成員函式的時機》中,我想要支援 mixed-mode arithmetic(混合模式運算),所以我們要讓下面這些程式碼能夠編譯。我們指望它能,因為我們使用了和 Item 24 中可以工作的程式碼相同的程式碼。僅有的區別是 Rational 和 operator* 現在是 templates(模板):

Rational oneHalf(1, 2); // this example is from 《C++箴言:宣告為非成員函式的時機》,
// except Rational is now a template

Rational result = oneHalf * 2; // error! won't compile

編譯失敗的事實暗示對於模板化 Rational 來說,有某些東西和 non-template(非模板)版本不同,而且確實存在。在《C++箴言:宣告為非成員函式的時機》中,編譯器知道我們想要呼叫什麼函式(取得兩個 Rationals 的 operator*),但是在這裡,編譯器不知道我們想要呼叫哪個函式。作為替代,它們試圖斷定要從名為 operator* 的 template(模板)中例項化出(也就是建立)什麼函式。它們知道它們假定例項化出的某個名為 operator* 的函式取得兩個 Rational 型別的引數,但是為了做這個例項化,它們必須斷定 T 是什麼。問題在於,它們做不到。

在推演 T 的嘗試中,它們會察看被傳入 operator* 的呼叫的 arguments(實參)的型別。在當前情況下,型別為 Rational(oneHalf 的型別)和 int(2 的型別)。每一個引數被分別考察。

使用 oneHalf 的推演很簡單。operator* 的第一個 parameter(形參)被宣告為 Rational 型別,而傳入 operator* 的第一個 argument(實參)(oneHalf) 是 Rational 型別,所以 T 一定是 int。不幸的是,對其它引數的推演沒那麼簡單。operator* 的第二個 parameter(形參)被宣告為 Rational 型別,但是傳入 operator* 的第二個 argument(實參)(2) 的 int 型別。在這種情況下,讓編譯器如何斷定 T 是什麼呢?你可能期望它們會使用 Rational 的 non-explicit constructor(非顯式建構函式)將2 轉換成一個 Rational,這樣就使它們推演出 T 是 int,但是它們不這樣做。它們不這樣做是因為在 template argument deduction(模板實參推演)過程中從不考慮 implicit type conversion functions(隱式型別轉換函式)。

這樣的轉換可用於函式呼叫過程,這沒錯,但是在你可以呼叫一個函式之前,你必須知道哪個函式存在。為了知道這些,你必須為相關的 function templates(函式模板)推演出 parameter types(引數型別)(以便你可以例項化出合適的函式)。但是在 template argument deduction(模板實參推演)過程中不考慮經由 constructor(建構函式)呼叫的 implicit type conversion(隱式型別轉換)。《C++箴言:宣告為非成員函式的時機》不包括 templates(模板),所以 template argument deduction(模板實參推演)不是一個問題,現在我們在 C++ 的 template 部分,這是主要問題。

在一個 template class(模板類)中的一個 friend declaration(友元宣告)可以指涉到一個特定的函式,我們可以利用這一事實為受到 template argument deduction(模板實參推演)挑戰的編譯器解圍。這就意味著 class Rational 可以為 Rational 宣告作為一個 friend function(友元函式)的 operator*。class templates(類别範本)不依靠 template argument deduction(模板實參推演)(這個過程僅適用於 function templates(函式模板)),所以 T 在 class Rational 被例項化時總是已知的。透過將適當的 operator* 宣告為 Rational class 的一個 friend(友元)使其變得容易:
template
class Rational {
public:
...
friend // declare operator*
const Rational operator*(const Rational& lhs, // function (see
const Rational& rhs); // below for details)
};

template // define operator*
const Rational operator*(const Rational& lhs, // functions
const Rational& rhs)
{ ... }

現在我們對 operator* 的混合模式呼叫可以編譯了,因為當 object oneHalf 被宣告為 Rational 型別時,class Rational 被例項化,而作為這一過程的一部分,取得 Rational parameters(形參)的 friend function(友元函式)operator* 被自動宣告。作為已宣告函式(並非一個 function template(函式模板)),在呼叫它的時候編譯器可以使用 implicit conversion functions(隱式轉換函式)(譬如 Rational 的 non-explicit constructor(非顯式建構函式)),而這就是它們如何使得混合模式呼叫成功的。

唉,在這裡的上下文中,“成功”是一個可笑的詞,因為儘管程式碼可以編譯,但是不能連線。但是我們過一會兒再處理它,首先我想討論一下用於在 Rational 內宣告 operator* 的語法。

在一個 class template(類别範本)內部,template(模板)的名字可以被用做 template(模板)和它的 parameters(引數)的縮寫,所以,在 Rational 內部,我們可以只寫 Rational 代替 Rational。在本例中這隻為我們節省了幾個字元,但是當有多個引數或有更長的引數名時,這既能節省擊鍵次數又能使最終的程式碼顯得更清晰。我把這一點提前,是因為 operator* 被宣告為取得並返回 Rationals,而不是 Rationals。它就像如下這樣宣告 operator* 一樣合法:

template
class Rational {
public:
...
friend
const Rational operator*(const Rational& lhs,
const Rational& rhs);
...
};

然而,使用縮寫形式更簡單(而且更常用)。

現在返回到連線問題。混合模式程式碼編譯,因為編譯器知道我們想要呼叫一個特定的函式(取得一個 Rational 和一個 Rational 的 operator*),但是那個函式只是在 Rational 內部宣告,而沒有在此處定義。我們的意圖是讓 class 之外的 operator* template(模板)提供這個定義,但是這種方法不能工作。如果我們自己宣告一個函式(這就是我們在 Rational template(模板)內部所做的事),我們就有責任定義這個函式。當前情況是,我們沒有提供定義,這也就是聯結器為什麼不能找到它。

讓它能工作的最簡單的方法或許就是將 operator* 的本體合併到它的 declaration(定義)中:

template
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
} //《C++箴言:宣告為非成員函式的時機》
};

確實,這樣就可以符合預期地工作:對 operator* 的混合模式呼叫現在可以編譯,連線,並執行。萬歲!

關於此技術的一個有趣的觀察結論是 friendship 的使用對於訪問 class 的 non-public parts(非公有構件)的需求並沒有起到什麼作用。為了讓所有 arguments(實參)的 type conversions(型別轉換)成為可能,我們需要一個 non-member function(非成員函式)(《C++箴言:宣告為非成員函式的時機》 依然適用);而為了能自動例項化出適當的函式,我們需要在 class 內部宣告這個函式。在一個 class 內部宣告一個 non-member function(非成員函式)的唯一方法就是把它做成一個 friend(友元)。那麼這就是我們做的。反傳統嗎?是的。有效嗎?毫無疑問。

就像《C++箴言:理解inline化的介入和排除》闡述的,定義在一個 class 內部的函式被隱式地宣告為 inline(內聯),而這也包括像 operator* 這樣的 friend functions(友元函式)。你可以讓 operator* 不做什麼事情,只是呼叫一個定義在這個 class 之外的 helper function(輔助函式),從而讓這樣的 inline declarations(內聯宣告)的影響最小化。在本文的這個示例中,沒有特別指出這樣做,因為 operator* 已經可以實現為一個 one-line function(單行函式),但是對於更復雜的函式體,這樣做也許是合適的。"have the friend call a helper"(“讓友元呼叫輔助函式”)的方法還是值得注意一下的。

Rational 是一個 template(模板)的事實意味著那個 helper function(輔助函式)通常也是一個 template(模板),所以典型情況下在標頭檔案中定義 Rational 的程式碼看起來大致如下:

template class Rational; // declare
// Rational
// template
template // declare
const Rational doMultiply(const Rational& lhs, // helper
const Rational& rhs); // template
template
class Rational {
public:
...

friend
const Rational operator*(const Rational& lhs,
const Rational& rhs) // Have friend
{ return doMultiply(lhs, rhs); } // call helper
...
};

多數編譯器基本上會強迫你把所有的 template definitions(模板定義)都放在標頭檔案中,所以你可能同樣需要在你的標頭檔案中定義 doMultiply。(就像 Item 30 闡述的,這樣的 templates(模澹┎恍枰?inline(內聯)。)可能看起來就像這樣:

template // define
const Rational doMultiply(const Rational& lhs, // helper
const Rational& rhs) // template in
{ // header file,
return Rational(lhs.numerator() * rhs.numerator(), // if necessary
lhs.denominator() * rhs.denominator());
}

當然,作為一個 template(模板),doMultiply 不支援混合模式乘法,但是它不需要。它只被 operator* 呼叫,而 operator* 支援混合模式運算!本質上,函式 operator* 支援為了確保被相乘的是兩個 Rational objects 而必需的各種 type conversions(型別轉換),然後它將這兩個 objects 傳遞給一個 doMultiply template(模板)的適當的例項化來做實際的乘法。配合行動,不是嗎?
Things to Remember
在寫一個提供了 class template(類别範本),而這個 class template(類别範本)提供了一個函式,這個函式指涉到支援所有 parameters(引數)的 implicit type conversions(隱式型別轉換)的 template(模板)的時候,把這些函式定義為 class template(類别範本)內部的 friends(友元)。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10617731/viewspace-957609/,如需轉載,請註明出處,否則將追究法律責任。

相關文章