C++類型轉(zhuǎn)換時定義非成員函數(shù)

字號:

《C++箴言:聲明為非成員函數(shù)的時機》闡述了為什么只有 non-member functions(非成員函數(shù))適合于應(yīng)用到所有 arguments(實參)的 implicit type conversions(隱式類型轉(zhuǎn)換),而且它還作為一個示例使用了一個 Rational class 的 operator* function。我建議你在閱讀本文之前先熟悉那個示例,因為本文進(jìn)行了針對《C++箴言:聲明為非成員函數(shù)的時機》中的示例做了一個無傷大雅(模板化 Rational 和 operator*)的擴(kuò)展討論:
    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++箴言:避免返回對象內(nèi)部構(gòu)件的句柄》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++箴言:聲明為非成員函數(shù)的時機》中,我想要支持 mixed-mode arithmetic(混合模式運算),所以我們要讓下面這些代碼能夠編譯。我們指望它能,因為我們使用了和 Item 24 中可以工作的代碼相同的代碼。僅有的區(qū)別是 Rational 和 operator* 現(xiàn)在是 templates(模板):
    Rational oneHalf(1, 2); // this example is from 《C++箴言:聲明為非成員函數(shù)的時機》,
    // except Rational is now a template
    Rational result = oneHalf * 2; // error! won’t compile
    編譯失敗的事實暗示對于模板化 Rational 來說,有某些東西和 non-template(非模板)版本不同,而且確實存在。在《C++箴言:聲明為非成員函數(shù)的時機》中,編譯器知道我們想要調(diào)用什么函數(shù)(取得兩個 Rationals 的 operator*),但是在這里,編譯器不知道我們想要調(diào)用哪個函數(shù)。作為替代,它們試圖斷定要從名為 operator* 的 template(模板)中實例化出(也就是創(chuàng)建)什么函數(shù)。它們知道它們假定實例化出的某個名為 operator* 的函數(shù)取得兩個 Rational 類型的參數(shù),但是為了做這個實例化,它們必須斷定 T 是什么。問題在于,它們做不到。
    在推演 T 的嘗試中,它們會察看被傳入 operator* 的調(diào)用的 arguments(實參)的類型。在當(dāng)前情況下,類型為 Rational(oneHalf 的類型)和 int(2 的類型)。每一個參數(shù)被分別考察。
    使用 oneHalf 的推演很簡單。operator* 的第一個 parameter(形參)被聲明為 Rational 類型,而傳入 operator* 的第一個 argument(實參)(oneHalf) 是 Rational 類型,所以 T 一定是 int。不幸的是,對其它參數(shù)的推演沒那么簡單。operator* 的第二個 parameter(形參)被聲明為 Rational 類型,但是傳入 operator* 的第二個 argument(實參)(2) 的 int 類型。在這種情況下,讓編譯器如何斷定 T 是什么呢?你可能期望它們會使用 Rational 的 non-explicit constructor(非顯式構(gòu)造函數(shù))將 2 轉(zhuǎn)換成一個 Rational,這樣就使它們推演出 T 是 int,但是它們不這樣做。它們不這樣做是因為在 template argument deduction(模板實參推演)過程中從不考慮 implicit type conversion functions(隱式類型轉(zhuǎn)換函數(shù))。從不。這樣的轉(zhuǎn)換可用于函數(shù)調(diào)用過程,這沒錯,但是在你可以調(diào)用一個函數(shù)之前,你必須知道哪個函數(shù)存在。為了知道這些,你必須為相關(guān)的 function templates(函數(shù)模板)推演出 parameter types(參數(shù)類型)(以便你可以實例化出合適的函數(shù))。但是在 template argument deduction(模板實參推演)過程中不考慮經(jīng)由 constructor(構(gòu)造函數(shù))調(diào)用的 implicit type conversion(隱式類型轉(zhuǎn)換)?!禖++箴言:聲明為非成員函數(shù)的時機》不包括 templates(模板),所以 template argument deduction(模板實參推演)不是一個問題,現(xiàn)在我們在 C++ 的 template 部分,這是主要問題。
    在一個 template class(模板類)中的一個 friend declaration(友元聲明)可以指涉到一個特定的函數(shù),我們可以利用這一事實為受到 template argument deduction(模板實參推演)挑戰(zhàn)的編譯器解圍。這就意味著 class Rational 可以為 Rational 聲明作為一個 friend function(友元函數(shù))的 operator*。class templates(類模板)不依靠 template argument deduction(模板實參推演)(這個過程僅適用于 function templates(函數(shù)模板)),所以 T 在 class Rational 被實例化時總是已知的。通過將適當(dāng)?shù)?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)
    { ... }
    現(xiàn)在我們對 operator* 的混合模式調(diào)用可以編譯了,因為當(dāng) object oneHalf 被聲明為 Rational 類型時,class Rational 被實例化,而作為這一過程的一部分,取得 Rational parameters(形參)的 friend function(友元函數(shù))operator* 被自動聲明。作為已聲明函數(shù)(并非一個 function template(函數(shù)模板)),在調(diào)用它的時候編譯器可以使用 implicit conversion functions(隱式轉(zhuǎn)換函數(shù))(譬如 Rational 的 non-explicit constructor(非顯式構(gòu)造函數(shù))),而這就是它們?nèi)绾问沟没旌夏J秸{(diào)用成功的。
    唉,在這里的上下文中,“成功”是一個可笑的詞,因為盡管代碼可以編譯,但是不能連接。但是我們過一會兒再處理它,首先我想討論一下用于在 Rational 內(nèi)聲明 operator* 的語法。
    在一個 class template(類模板)內(nèi)部,template(模板)的名字可以被用做 template(模板)和它的 parameters(參數(shù))的縮寫,所以,在 Rational 內(nèi)部,我們可以只寫 Rational 代替 Rational。在本例中這只為我們節(jié)省了幾個字符,但是當(dāng)有多個參數(shù)或有更長的參數(shù)名時,這既能節(jié)省擊鍵次數(shù)又能使最終的代碼顯得更清晰。我把這一點提前,是因為 operator* 被聲明為取得并返回 Rationals,而不是 Rationals。它就像如下這樣聲明 operator* 一樣合法:
    template
    class Rational {
    public:
    ...
    friend
    const Rational operator*(const Rational& lhs,
    const Rational& rhs);
    ...
    };
    然而,使用縮寫形式更簡單(而且更常用)。
    現(xiàn)在返回到連接問題?;旌夏J酱a編譯,因為編譯器知道我們想要調(diào)用一個特定的函數(shù)(取得一個 Rational 和一個 Rational 的 operator*),但是那個函數(shù)只是在 Rational 內(nèi)部聲明,而沒有在此處定義。我們的意圖是讓 class 之外的 operator* template(模板)提供這個定義,但是這種方法不能工作。如果我們自己聲明一個函數(shù)(這就是我們在 Rational template(模板)內(nèi)部所做的事),我們就有責(zé)任定義這個函數(shù)。當(dāng)前情況是,我們沒有提供定義,這也就是連接器為什么不能找到它。
    讓它能工作的最簡單的方法或許就是將 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++箴言:聲明為非成員函數(shù)的時機》
    };
    確實,這樣就可以符合預(yù)期地工作:對 operator* 的混合模式調(diào)用現(xiàn)在可以編譯,連接,并運行。萬歲!
    關(guān)于此技術(shù)的一個有趣的觀察結(jié)論是 friendship 的使用對于訪問 class 的 non-public parts(非公有構(gòu)件)的需求并沒有起到什么作用。為了讓所有 arguments(實參)的 type conversions(類型轉(zhuǎn)換)成為可能,我們需要一個 non-member function(非成員函數(shù))(《C++箴言:聲明為非成員函數(shù)的時機》 依然適用);而為了能自動實例化出適當(dāng)?shù)暮瘮?shù),我們需要在 class 內(nèi)部聲明這個函數(shù)。在一個 class 內(nèi)部聲明一個 non-member function(非成員函數(shù))的方法就是把它做成一個 friend(友元)。那么這就是我們做的。反傳統(tǒng)嗎?是的。有效嗎?毫無疑問。
    就像《C++箴言:理解inline化的介入和排除》闡述的,定義在一個 class 內(nèi)部的函數(shù)被隱式地聲明為 inline(內(nèi)聯(lián)),而這也包括像 operator* 這樣的 friend functions(友元函數(shù))。你可以讓 operator* 不做什么事情,只是調(diào)用一個定義在這個 class 之外的 helper function(輔助函數(shù)),從而讓這樣的 inline declarations(內(nèi)聯(lián)聲明)的影響最小化。在本文的這個示例中,沒有特別指出這樣做,因為 operator* 已經(jīng)可以實現(xiàn)為一個 one-line function(單行函數(shù)),但是對于更復(fù)雜的函數(shù)體,這樣做也許是合適的。"have the friend call a helper"(“讓友元調(diào)用輔助函數(shù)”)的方法還是值得注意一下的。
    Rational 是一個 template(模板)的事實意味著那個 helper function(輔助函數(shù))通常也是一個 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
    ...
    };
    多數(shù)編譯器基本上會強迫你把所有的 template definitions(模板定義)都放在頭文件中,所以你可能同樣需要在你的頭文件中定義 doMultiply。(就像 Item 30 闡述的,這樣的 templates(模板)不需要 inline(內(nèi)聯(lián))。)可能看起來就像這樣:
    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());
    }
    當(dāng)然,作為一個 template(模板),doMultiply 不支持混合模式乘法,但是它不需要。它只被 operator* 調(diào)用,而 operator* 支持混合模式運算!本質(zhì)上,函數(shù) operator* 支持為了確保被相乘的是兩個 Rational objects 而必需的各種 type conversions(類型轉(zhuǎn)換),然后它將這兩個 objects 傳遞給一個 doMultiply template(模板)的適當(dāng)?shù)膶嵗瘉碜鰧嶋H的乘法。配合行動,不是嗎?
    Things to Remember
    ·在寫一個提供了 class template(類模板),而這個 class template(類模板)提供了一個函數(shù),這個函數(shù)指涉到支持所有 parameters(參數(shù))的 implicit type conversions(隱式類型轉(zhuǎn)換)的 template(模板)的時候,把這些函數(shù)定義為 class template(類模板)內(nèi)部的 friends(友元)。