1. 程式人生 > >為模版類增添友元函式的幾種方法

為模版類增添友元函式的幾種方法

《Effective C++ 3rd Edition》中的條款46提到了“需要型別轉換時請為模版定義非成員函式”,其中涉及到模版類和友元函式,我之前還從沒把這兩者聯絡在一起過。既然看到這裡,於是把相關的知識梳理了一下。為模版類新增的友元函式分為三類:

非模板友元
約束(bound)模板友元,即友元的型別取決於類被例項化時的型別。
非約束(undound)模板友元,即友元的所有具體化都是類的每一個具體化的友元。
書中作者的實現方式採用了第一種,先看原始程式碼:
template <typename T>
class Rational
{
public:
    Rational(const T &numerator=0, const T &denominator=1):
        _numerator(numerator), _denominator(denominator) {}
    const T numerator() const { return _numerator; }
    const T denominator() const { return _denominator; }
private:
    T _numerator;
    T _denominator;
};

template <typename T>
const Rational<T> operator *(const Rational<T> &lhs, const Rational<T> &rhs)
{
    return Rational<T>(lhs.numerator() * rhs.numerator(),
                       lhs.denominator() * rhs.denominator());
}

template <typename T>
ostream& operator <<(ostream &os, const Rational<T> &rational)
{
    os << "Numerator: " << rational.numerator() <<
        "  Denominator: " << rational.denominator();
    return os;
}

int main(void)
{
    Rational<int> a(3, 5), b(2, 7);
    Rational<int> c = a * b;
    cout << c << endl;
    return 0;
}
當operator *僅僅是一個普通函式模版而非友元時,上述Rational<int>的物件a和b相乘沒有問題,因為函式模版可以根據引數型別被具體化。然而改成a * 2就失敗了,雖然整數2可以隱式轉化為Rational<int>型別,但是編譯器在進行模版匹配的時候不會考慮這一點(假如要納入考慮,可以想象一下當引數有很多個又是不同型別,且都可以由其他型別隱式轉化而來,編譯器會有多頭痛),所以我們在最好讓 Rational<int> operator *(const Rational<int> &lhs, const Rational<int> &rhs) 這樣的函式被宣告出來,執行a * 2會直接和該函式適配,發現2可以隱式轉化成Rational<int>,OK成功。那麼最容易聯想到的方法自然是宣告成類的友元函式,這樣Rational<T>被具體化到Rational<int>的過程中,這個函式也就被宣告出來了,看程式碼:
template <typename T>
class Rational
{
public:
    Rational(const T &numerator=0, const T &denominator=1):
        _numerator(numerator), _denominator(denominator) {}
    const T numerator() const { return _numerator; }
    const T denominator() const { return _denominator; }
    friend const Rational operator *(const Rational &lhs, const Rational &rhs);
    //friend const Rational<T> operator *(const Rational<T> &lhs, const Rational<T> &rhs);
private:
    T _numerator;
    T _denominator;
};

template <typename T>
const Rational<T> operator *(const Rational<T> &lhs, const Rational<T> &rhs)
{
    return Rational<T>(lhs._numerator * rhs._numerator,
                       lhs._denominator * rhs._denominator);
}

template <typename T>
ostream& operator <<(ostream &os, const Rational<T> &rational)
{
    os << "Numerator: " << rational.numerator() <<
        "  Denominator: " << rational.denominator();
    return os;
}

int main(void)
{
    Rational<int> a(3, 5), b(2, 7);
    Rational<int> c = a * 2;
    cout << c << endl;
    return 0;
}
值得注意的是上述程式碼中註釋掉的那一行友元函式的寫法也是合理的,因為函式宣告本身在Rational模版類內部,所以返回值和引數用到的Rational<T>可以省去<T>。重點來了,這段程式碼雖然能通過編譯卻無法成功連結。可能很多人和我一樣,開始都會不假思索的認為operator *函式模版的定義也會被具體化,其實不然。友元函式雖然寫在類裡面,但是它並非一個成員函式,當類被具體化的時候,外面定義的operator *函式模版還是原封不動的,自然就無法連結了,除非完整實現一份當特化為int型別的友元函式。於是最後的方案是將定義的部分也挪進類裡面,如果一定要寫在外面,那隻好把整個函式的定義寫出來了,對不同的T需要寫多份,很麻煩。
template <typename T>
class Rational
{
public:
    Rational(const T &numerator=0, const T &denominator=1):
        _numerator(numerator), _denominator(denominator) {}
    const T numerator() const { return _numerator; }
    const T denominator() const { return _denominator; }
    friend const Rational operator *(const Rational &lhs, const Rational &rhs)
    {
        return Rational(lhs._numerator * rhs._numerator,
                        lhs._denominator * rhs._denominator);
    }
private:
    T _numerator;
    T _denominator;
};

接下來再看看另外兩種友元函式的實現,他們均被定義成函式模版。先看約束模版友元,雖然函式模版可以被具體化為多種函式,但只有指定的一種才能成為類的朋友,也就是說在宣告時直接指定一個型別,像下面這樣:

template <typename T>
class Rational;


template <typename T>
const Rational<T> operator *(const Rational<T> &, const Rational<T> &);

template <typename T>
class Rational
{
public:
    Rational(const T &numerator=0, const T &denominator=1):
        _numerator(numerator), _denominator(denominator) {}
    const T numerator() const { return _numerator; }
    const T denominator() const { return _denominator; }
    friend const Rational<T> operator * <>(const Rational<T> &lhs, const Rational<T> &rhs);

private:
    T _numerator;
    T _denominator;
};

template <typename T>
const Rational<T> operator *(const Rational<T> &lhs, const Rational<T> &rhs)
{
    return Rational<T>(lhs._numerator * rhs._numerator,
                       lhs._denominator * rhs._denominator);
}

template <typename T>
ostream& operator <<(ostream &os, const Rational<T> &rational)
{
    os << "Numerator: " << rational.numerator() <<
        "  Denominator: " << rational.denominator();
    return os;
}

int main(void)
{
    Rational<int> a(3, 5), b(2, 7);
    Rational<int> c = a * b;
    cout << c << endl;
    return 0;
}

有幾點值得注意的。既然宣告的友元是個特化的函式,那麼之前必須先前置宣告該函式模版,否則編譯器就不認識了。宣告的友元函式裡面有個尖括號<>,其實是省略了型別T,因為通過函式的引數和返回值可以進行推導,倘若是個類似 void fun(void) 的友元函式,那麼就得寫明。這裡我們指明瞭函式模版用型別T來特化,這就取決於模版類的特化,當然,如果有需要也可以特化為<int>或<char>或者其他。和前面那個例子不同的是,這時函式不能定義在類裡面,我用的VC2008編譯器會報告 Error C3637: a friend function definition cannot be a specialization of a function type。也就是說,上面這個方法,無法支援a * 2這種運算,最多支援a * b。個人認為約束模版友元也有其意義,比如一個模版類A<T>和函式模版fun<U>,當T為內建數值型別時,授權fun<CLASS_X>訪問,當T為使用者自定義型別時,授權fun<CLASS_Y>訪問,這樣就可以靈活授權給不同的友元函式,實現差異化的需求。

最後是非約束模版友元,在下面的例子裡,類模版的型別為T,友元函式模版的型別為U,兩者不相干,任意一個將U具體化的operator *函式,都是任何一個將T具體化的Rational類的朋友,這是一種“多對多”的關係。和上一個例子一樣,函式模版的定義部分在類的外部,進行匹配時編譯器不會考慮隱式轉化,所以也無法支援a * 2這樣的語句。

template <typename T>
class Rational
{
public:
    Rational(const T &numerator=0, const T &denominator=1):
        _numerator(numerator), _denominator(denominator) {}
    const T numerator() const { return _numerator; }
    const T denominator() const { return _denominator; }
    template <typename U>
    friend const Rational<U> operator *(const Rational<U> &lhs, const Rational<U> &rhs);

private:
    T _numerator;
    T _denominator;
};

template <typename U>
const Rational<U> operator *(const Rational<U> &lhs, const Rational<U> &rhs)
{
    return Rational<U>(lhs._numerator * rhs._numerator,
                       lhs._denominator * rhs._denominator);
}

template <typename T>
ostream& operator <<(ostream &os, const Rational<T> &rational)
{
    os << "Numerator: " << rational.numerator() <<
        "  Denominator: " << rational.denominator();
    return os;
}

模版類的友元函式有這麼幾種情況,如果需要宣告的是一個友元類,且這個類有可能是一個模版,那麼同樣會有多種情況。總之,有了模版這個東西,C++更強大,也更復雜了。