Item 23: 用非成員非友元函數取代成員函數
想象一個象徵 web 瀏覽器的類。在大量的函數中,這樣一個類也許會提供清空已下載成分的緩存。清空已訪問 URLs 的歷史,以及從系統移除所有 cookies 的功能:
class WebBrowser {
public:
...
void clearCache();
void clearHistory();
void removeCookies();
...
};
很多用戶希望能一起執行全部這些動作,所以 WebBrowser 可能也會提供一個函數去這樣做:
class WebBrowser {
public:
...
void clearEverything(); // calls clearCache, clearHistory,
// and removeCookies
...
};
當然,這個功能也能通過非成員函數調用適當的成員函數來提供:
void clearBrowser(WebBrowser& wb)
{
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
那麼哪個更好呢,成員函數 clearEverything 還是非成員函數 clearBrowser?
從封裝性來說,因爲clearBrowser不能訪問類的任何私有成員,因此clearBrowser(非成員非友元函數)比 clearEverything(成員函數)更可取:它能爲 WebBrowser 獲得更強的封裝性。
可以採用以下的方式來定義:
namespace WebBrowserStuff {
class WebBrowser { ... };
void clearBrowser(WebBrowser& wb);
...
}
將clearBrowser和class WebBrowser放在同一個名字空間內。
一個類似 WebBrowser 的類可以有大量的方便性函數,一些是書籤相關的,另一些打印相關的,還有一些是 cookie 管理相關的,等等。作爲一個一般的慣例,多數客戶僅對這些方便性函數的集合中的一些感興趣。沒有理由讓一個只對書籤相關的方便性函數感興趣的客戶在編譯時依賴其它函數,例如,cookie 相關的方便性函數。分隔它們的直截了當的方法就是在一個頭文件中聲明書籤相關的方便性函數,在另一個不同的頭文件中聲明 cookie 相關的方便性函數,在第三個頭文件聲明打印相關的方便性函數,等等:
// header "webbrowser.h" - header for class WebBrowser itself
// as well as "core" WebBrowser-related functionality
namespace WebBrowserStuff {
class WebBrowser { ... };
... // "core" related functionality, e.g.
// non-member functions almost
// all clients need
}
// header "webbrowserbookmarks.h"
namespace WebBrowserStuff {
... // bookmark-related convenience
} // functions
// header "webbrowsercookies.h"
namespace WebBrowserStuff {
... // cookie-related convenience
} // functions
將所有方便性函數放入多個頭文件中——但是在一個 namespace 中——也意味着客戶能容易地擴充方便性函數的集合。他們必須做的全部就是在 namespace 中加入更多的非成員非友元函數。
總結:
用非成員非友元函數取代成員函數。這樣做可以提高封裝性,包裹彈性,和機能擴充性。
Item 24: 當所有參數皆需類型轉換,請爲此採用非成員函數
設計一個用來表現有理數的類
class Rational {
public:
Rational(int numerator = 0, // ctor is deliberately not explicit;
int denominator = 1); // allows implicit int-to-Rational
// conversions
int numerator() const; // accessors for numerator and
int denominator() const; // denominator - see Item 22
private:
...
};
研究一下讓 operator* 成爲 Rational 的一個成員函數的想法究竟如何:
class Rational {
public:
...
const Rational operator*(const Rational& rhs) const;
};
這個設計讓你在有理數相乘時不費吹灰之力:
Rational oneEighth(1, 8);
Rational oneHalf(1, 2);
Rational result = oneHalf * oneEighth; // fine
result = result * oneEighth; // fine
當你試圖做混合模式的算術運算時,可是,你發現只有一半時間它能工作:
result = oneHalf * 2; // fine
result = 2 * oneHalf; // error!
當你重寫最後兩個例子爲功能等價的另一種形式時,問題的來源就變得很明顯了:
result = oneHalf.operator*(2); // fine
result = 2.operator*(oneHalf); // error!
對象 oneHalf 是一個包含 operator* 的類的實例,所以編譯器調用那個函數。然而,整數 2 與類沒有關係,因而沒有 operator* 成員函數。編譯器同樣要尋找能如下調用的非成員的 operator*s(也就是說,在 namespace 或全局範圍內的 operator*s):
result = operator*(2, oneHalf); // error!
但是在本例中,沒有非成員的持有一個 int 和一個 Rational 的 operator*,所以搜索失敗。
當然,編譯器這樣做僅僅是因爲提供了一個非顯性的構造函數。如果 Rational 的構造函數是顯性的,這些語句都將無法編譯:
result = oneHalf * 2; // error! (with explicit ctor);
// can't convert 2 to Rational
result = 2 * oneHalf; // same error, same problem
支持混合模式操作失敗了,但是至少兩個語句的行爲將步調一致。
然而,你的目標是既保持一致性又要支持混合運算,也就是說,一個能使上面兩個語句都可以編譯的設計。讓我們返回這兩個語句看一看,爲什麼即使 Rational 的構造函數不是顯式的,也是一個可以編譯而另一個不行:
result = oneHalf * 2; // fine (with non-explicit ctor)
result = 2 * oneHalf; // error! (even with non-explicit ctor)
其原因在於僅僅當參數列在參數列表中的時候,它們纔有資格進行隱式類型轉換。而對應於成員函數被調用的那個對象的隱含參數—— this 指針指向的那個——根本沒有資格進行隱式轉換。這就是爲什麼第一個調用能編譯而第二個不能。第一種情況包括一個參數被列在參數列表中,而第二種情況沒有。
你還是希望支持混合運算,然而,現在做到這一點的方法或許很清楚了:讓 operator* 作爲非成員函數,因此就允許便一起將隱式類型轉換應用於所有參數:
class Rational {
... // contains no operator*
};
const Rational operator*(const Rational& lhs, // now a non-member
const Rational& rhs) // function
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
Rational oneFourth(1, 4);
Rational result;
result = oneFourth * 2; // fine
result = 2 * oneFourth; // hooray, it works!
另外,operator* 完全能夠根據 Rational 的 public 接口完全實現,因此無需將其聲明爲友元函數。上面的代碼展示了做這件事的方法之一。這導出了一條重要的結論:與成員函數相對的是非成員函數,而不是友元函數。
總結:
如果你需要在一個函數的所有參數(包括被 this 指針所指向的那個)上使用類型轉換,這個函數必須是一個非成員函數。