Introduction
這一章主要講述瞭如何去設計對象, 接口.
Rule 18: Make Interfaces easy to use correctly and hard to use incorrectly
在設計接口的時候, 應該儘量做到, 如果使用者能夠通過編譯, 那麼就應該能得到他所期望的結果. 如果有任何錯誤, 應該儘量提前至編譯期就能夠報出來.
例如:
class Date{
public:
Date(int month, int day, int year);
…
}
在使用時, 可能客戶會弄錯 day 和 month 的位置, 產生錯誤, 那麼可以定義 類型系統(type system)
來避免這種問題, 定義類似於 struct Day, struct Month, struct Year
, 並且在類型系統中限制變量的範圍等.
在設定資源獲取接口的時候, 通過返回 shared_ptr
能夠阻止一大羣用戶犯下資源泄漏的問題.
std::tr1::shared_ptr<Investment> createInvestment()
{
std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0), getRidOfInvestment); // declare null pointer
retVal = ...; // point to the right object
return retVal;
}
如果能夠直接將pInv指向正確的對象會比先指向null, 再指向正確的對象更好. 這樣做最大的好處是能夠放置 “cross-DLL problem
“, 就是解決 “跨 DLL 之 new/delete 成對應用” 的問題, 經常我們在一個文件new了卻忘了在另一個文件delete, 使用 shared_ptr 可以避免這一點.
Remeber:
- 設計接口應該容易被正確使用, 不容易被錯誤使用. 當錯誤使用的時候應該儘可能提前報錯
- “促進正確使用” 的辦法包括接口的一致性,以及與內置類型的行爲兼容
- “阻止勿用” 的辦法包括建立新類型, 限制類型上的操作, 束縛對象值, 以及消除客戶的資源管理責任
- shared_ptr 支持定製型刪除器( custom deleter ), 可防範 cross-DLL problem
Rule 19: Treat class design as type design
在設計一個類之前, 請先考慮如下問題:
- 新type的對象應該如何被創建和銷燬? 影響到你的構造函數, 析構函數, 以及內存分配函數和釋放函數.
- 對象的初始化和賦值有怎樣的區別? 區分出如何書寫 copy assignment 函數
- 對象如果被pass-by-value的時候意味着什麼? 思考如何書寫拷貝構造函數
- 什麼是新 type 的”合法值”? 考慮建立值的約束, 異常拋出等
- 新type需要配合某個繼承圖系( inheritance graph )嗎? 影響到 virtual 函數
- 新type需要怎樣的轉換? 對於彼此有轉換行爲的類型, 需仔細考慮
- 什麼樣的操作符對於新type是合理的? 考慮到重載操作符等
- 誰會使用新type的成員? 決定成員的 public, private, protected, 也幫助決定哪個 classes 或 functions 是 friends.
- 什麼是新type的”未聲明接口” (undeclared interface)? 它對效率, 異常安全以及資源是用提供何種保證?
- 你的新type有多麼一般化? 考慮到 template 的問題
- 你真的需要一個新type嗎? 如果只是新加個簡單的機能, 可能單純定一個 non-member 函數或 templates 更有效
Remeber:
Class的設計就是type的設計. 在定義一個新type之前, 請確定你已經考慮過了本條款覆蓋的所有討論主題.
Rule 20: Prefer pass-by-reference-to-const to pass-by-value
slicing problem說的是當一個derived對象以by-value方式傳遞並被視爲一個base class對象的時候, base class對象的copy構造函數會被調用, 並且 “造成此對象的行爲像一個 derived class對象”
Remeber:
- 儘量以 pass-by-reference-to-const 替換 pass-by-value, 前者通常比後者高效, 並且避免切割問題(slicing problem)
- 以上規則並不適用於內置類型, 以及STL的迭代器和函數對象. 對它們而言, pass-by-value 往往比較合適
Rule 21: Don’t try to return a reference when you must return an object
在該返回值的時候還是需要返回值的. 一般返回有以下幾種可能:
- 返回值. 承受該有的構造和析構成本
- 返回指向 heap-allocated 對象的指針. 傳遞指針的方式會快一點.
- 返回指向 static 變量的 reference. 一般用於單例模式.
Remeber:
絕對不要返回一個pointer或reference指向一個local stack對象, 或返回一個 reference 指向一個 heap-allocated 對象, 或返回 pointer 或 reference 指向一個 local static 對象但是卻不是單例.
Rule 22: Declare data members private
成員變量的封裝性與”成員變量的內容改變時所破壞的代碼數量”成正比. 所以無論是使用protect還是public, 當一個變量移除的時候, 所有使用它的客戶代碼都會被破壞. 而如果全部使用函數來獲取變量, 那麼可以通過別的途徑來實現這個函數.
Remeber:
- 切記將成員變量聲明爲 private. 這可賦予客戶訪問數據的一致性, 可細微劃分訪問控制, 允諾約束條件獲得保證, 並提供 class 作者以充分的實現彈性.
- protected 並不比 public 更具封裝性.
Rule 23: Prefer non-member non-friend functions to member functions
舉個例子, 對於瀏覽器:
class WebBrowser{
public:
...
void clearCache();
void clearHistory();
void removeCookies();
...
}
如果要寫一個函數, 執行以上三個成員函數, 那麼我是寫一個 clearEverything() 作爲成員函數好一些呢? 還是寫一個 non-member, non-friend 函數好一些呢?
封裝性可理解爲, 讓儘量少的函數可以訪問類的數據.
從這種層面上來說, non-member, non-friend 函數會好一些.
Remeber:
non-member non-friend functions 相較於 member functions 更好, 能夠增加類的封裝性, 包裹彈性( packaging flexibility ).
Rule 24: Declare non-member functions when type conversions should apply to all parameters
如果你需要爲某個函數的所有參數進行類型轉換的, 那麼這個函數必須是個 non-member 的.
以有理數與整數相乘爲例子:
class Rational{
public:
Rational(int numerator = 0, int denominator = 1); /// 構造函數不爲 explicit, 允許 int-to-Rational 隱式轉換
int numerator() const;
int denominator() const;
…
private:
…
};
如果將operator* 作爲成員函數來寫的話:
class Rational{
public:
...
const Rational operator* (const Rational& rhs) const;
};
那麼在類型轉換的時候會出現問題:
Rational oneHalf(1,2);
Rational result;
result = onHalf*2; // 沒問題, 會先將 2 隱式轉換爲 Rational, 然後再相乘
result = 2* onHalf; // 報錯, Rational不在int構造函數參數列表中, 不會進行隱式類型轉換.
在這種時候, 需要對operator所有參數均進行類型轉換, 只能通過 non-member function 來實現:
class Rational{
...
};
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
這樣就能解決剛剛類型轉換的問題, 那麼這個函數要不要設置爲friend函數呢? 在不需要使用到class成員變量就能夠達到目的的情況下, 不需要設置爲 friend , 可以提高封裝性.
Remember:
如果你需要爲某個函數的所有參數進行類型轉換的(包括this指針所隱喻參數), 那麼這個函數必須是個 non-member 的.
Rule 25: Consider support for a non-throwing swap
這條rule解釋起來比較麻煩. 我們先要了解 std swap 是如何實現的:
template<typename T>
void swap(T &a, T &b){
T temp(a);
a = b;
b = temp;
}
整個過程包括3次複製, 所以效率在某些情況是很低的. 比如一個類裏面只有一個指針的時候, swap實質上只是交換指針所指的東西, 而通過 std::swap 則還會複製指針所指向的內容, 造成效率低下:
class Widget{
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs)
{
...
*pImpl = *(rhs.pImpl); //複製了指針指向之物
...
}
private:
WidgetImpl* pImpl;
}
對於上面這種情況, 就需要製作自己特化的swap了. 遵從以下步驟:
提供一個 public swap 函數, 並保證不拋出異常.
class Widget{ public: ... void swap(Widget& other) { using std::swap; swap(pImpl, other.pImpl); } ... };
在你的 class 的命名空間內提供一個 non-member swap, 並令它調用上述 swap成員函數.
namespace WidgetStuff{ ... template<typename T> class Widget { ... }; ... template<typename T> void swap(Widget<T>& a, Widget<T>& b) { a.swap(b); } }
如果你正在編寫一個 class (而非 class template), 爲你的 class 特化 std::swap. 並令它調用你的 swap 成員函數.
namespace std{ template<> // 修訂後的 std::swap 特化版本 void swap<Widget>( Widget &a, Widget &b) { a.swap(b); } }
Remeber:
- 當 std::swap 對你的類型效率不高的時候, 提供一個 public swap 成員函數, 並確保該函數不拋出異常.
- 然後再實現一個 non-member swap 來調用前者, 對於 classes(而非 templates), 也請特化 std::swap
- 調用swap的時候, 請先使用 using std::swap, 然後再不帶任何”命名空間資格修飾”調用swap