- 條款17:讓介面容易被正確使用,不易被誤用(Make interfaces easy to use correctly and hard to use incorrectly)
- 限制型別和值
- 規定能做和不能做的事
- 提供行為一致的介面
- 條款19:設計class猶如設計type(Treat class design as type design)
- 條款20:寧以pass-by-reference-to-const替換pass-by-value(Prefer pass-by-reference-to-cons to pass-by-value)
- 避免構造和析構
- 避免物件切割
- 例外
- 條款21:必須返回物件時,別妄想返回其reference(Don’t try to return a reference when you must return an object)
- 條款22:成員變數宣告為private(Declare data members private)
- 條款23:寧以non-member、non-friend替換member函式(Prefer non-member non-friend functions to member functions)
- 條款24:若所有引數皆需型別轉換,請為此採用non-member函式(Declare non-member functions when type conversions should apply to all parameters)
- 條款25:考慮寫出一個不拋異常的swap函式(Consider support for a non-throwing swap)
- 預設的swap
- 特化的swap
- 使用swap的總結
條款17:讓介面容易被正確使用,不易被誤用(Make interfaces easy to use correctly and hard to use incorrectly)
限制型別和值
class Date {
public:
Date(int month, int day, int year); //可能月日年順序錯,可能傳遞無效的月份或日期
...
};
可使用型別系統(type system)規避以上錯誤,即引入外覆型別(wrapper type)區別年月日:
struct Day {
explicite Day(int d)
: val(d) { }
int val;
}
struct Month {
explicite Month(int m)
: val(m) { }
int val;
}
struct Year{
explicite Year(int y)
: val(y) { }
int val;
}
class Date {
public:
Date(const Month& m, const Day& d, const Year& y); //可能月日年順序錯,可能傳遞無效的月份或日期
...
};
Date d(Month(3), Day(30), Year(1995)); //可有效防止介面誤用
保證了型別正確之後,需要保證輸入的值有效:
class Month {
public:
static Month Jan() { return Month(1); }
static Month Feb() { return Month(2); }
...
static Month Dec() { return Month(12); }
...
private:
explicit Month(int m);
...
};
Date d(Month::Mar(), Day(30), Year(1995));
規定能做和不能做的事
if ( a * b = c) ... //以const修飾運算子*,使其不能被賦值
提供行為一致的介面
為了避免忘記刪除或者重複刪除指標,可令工廠函式直接返回智慧指標:
Investment* createInvestment(); //使用者可能忘記刪除或者重複刪除指標
std::tr1::shared_ptr<Investment> createInvestment();
若期望用自定義的getRidOfInvestment,則需要避免誤用delete,可考慮將getRidOfInvestment繫結為刪除器(deleter):
刪除器在引用次數為0時呼叫,故可建立一個null shared_ptr
std::tr1::shared_ptr<Investment> createInvestment()
{
std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0),
getRidOfInvestment); //建立一個null shared_ptr
retVal = ... ; //令retVal指向目標物件
return retVal;
}
若pInv管理的原始指標能在pInv創立之前確定下來,則將原始指標直接傳遞給pInv的建構函式更好
tr1::shared_ptr會自動使用每個指標專屬的刪除器,從而無須擔心cross-DLL problem:
cross-DLL problem:物件在動態連線程式庫(DLL)中被new建立,但在另一個DLL內被delete銷燬
//返回的tr1::shared_ptr可能被傳遞給任何其他DLL
//其會追蹤記錄從而在引用次數為0時呼叫那個DLL的delete
std::tr1:;shared_ptr<Investment> createInvestment()
{
return std::tr1::shared_ptr<Investment>(new Stock);
}
Boost的tr1::shared_ptr特點:
- 是原始指標的兩倍大
- 以動態分配記憶體作為簿記用途和刪除器的專屬資料
- 以virtual形式呼叫刪除器
- 在多執行緒程式修改引用次數時有執行緒同步化(thread synchronization)的額外開銷
Tips:
- 好的介面不易被誤用
- 促進正確使用的方法包括介面一致性和與內建型別的行為相容
- 阻止誤用的辦法包括建立新型別、限制型別上的操作、束縛物件值、消除客戶的資源管理責任
- tr1::shared_ptr支援定製型刪除器(custom deleter),這可以防範DLL問題,可被用來自動解除互斥鎖(mutexes)等
條款19:設計class猶如設計type(Treat class design as type design)
定義一個新class時也就定義了一個新type。設計高效的類需要考慮以下問題:
- 新type的物件應如何建立和銷燬(第8章))
- 影響建構函式和解構函式、記憶體分配函式和釋放函式(operator new,operator new [],operator delete,operator delete [])
- 物件的初始化和賦值應有什麼差別(條款4)
- 決定建構函式和賦值運算子的行為
- 新type的物件如果被pass-by-value意味著什麼
- 由copy建構函式定義pass-by-value如何實現
- 什麼是新type的合法值
- 有效的數值集決定了類必須維護的約束條件(invariants),
- 進而決定了成員函式(特別是建構函式、解構函式、setter函式)的錯誤檢查
- 還影響函式丟擲的異常和極少使用的函式異常明細列(exception specifications)
- 有效的數值集決定了類必須維護的約束條件(invariants),
- 新type需要配合某個繼承圖系(inheritance graph)嗎
- 繼承既有的類,則受那些類束縛,尤其要考慮那些類的函式是否為虛擬函式
- 被其他類繼承,則影響解構函式等是否為virtual
- 新type需要什麼樣的轉換
- 若允許型別T1隱式轉換為型別T2,可可考慮:
- 在T1類內寫型別轉換函式(operator T2)
- 在T2類內些non-explicit-one-argument(可被單一實參呼叫)的建構函式
- 若只允許explicit建構函式存在,就得寫專門執行轉換的函式,且沒有型別轉換運算子(type conversion operators)或non-explicit-one-argument建構函式
- 若允許型別T1隱式轉換為型別T2,可可考慮:
- 什麼樣的運算子和函式對於此新type合理
- 決定需要宣告哪些函式,其中哪些是成員函式
- 什麼樣的標準函式應駁回
- 這些必須宣告為private
- 誰改取用新type的成員
- 影響public、private、protected的選擇
- 影響友元類、友元函式、及其巢狀的設計
- 什麼是新type的未宣告介面(undeclared interface)
- 要考慮其對效率、異常安全性、資源運用的保證
- 新type有多麼一般化
- 若要定義整個type家族,則應該定義新的class template
- 是否真的需要新type
- 若定義新的派生類就足夠,則可能定義non-member函式或templates更好
Tips:
- Class設計就是type設計,需要考慮以上所有問題
條款20:寧以pass-by-reference-to-const替換pass-by-value(Prefer pass-by-reference-to-cons to pass-by-value)
避免構造和析構
class Person {
public:
Person();
virtual ~Person();
...
private:
std::string name;
std::string address;
};
class Student: public Person {
public:
Student();
~Student();
...
private:
std::string schoolName;
std::string schoolAddress;
};
bool validateStudent(Student s); //會呼叫六次建構函式和六次解構函式
bool validateStudent(const Student& s); //效率提升很多
上述程式碼validateStudent函式中pass-by-value會呼叫六次建構函式和六次解構函式:
- Student構造+Person構造+Student的2個string+Person的2個string
- 析構同理
使用pass-by-reference可避免頻繁構造和析構
避免物件切割
物件切割(slicing):派生類以值傳遞並被視為基類物件時,回撥用基類的建構函式,而派生類的成分全無
class Window {
public:
...
std::string name() const; //返回視窗名稱
virtual void display() const; //顯示視窗和其內容
};
class WindowWithScrollBars: public Window {
public:
...
virtual void display() const;
};
void printNameAndDisply(Window w)
{
std::cout << w,name();
w.display();
}
WindowWithScrollBars wwsb;
printNameAndDisply(wwsb); //物件會被切割,因為引數w時Window物件,故呼叫的Window::display
void printNameAndDisply(const Window& w) //不會被切割
{
std::cout << w,name();
w.display();
}
例外
- 內建型別和STL的迭代器與函式物件採用pass by value往往效率更高,
- 小型type不一定適合pass by value
- 一旦需要複製指標的所指物,則copy建構函式可能成本很高
- 即使小型物件的copy建構函式不昂貴,其效率也存在爭議
- 某些編譯器對內建型別和自定義型別的態度截然不同,即使二者底層表示(underlying representation)相同
- 如可能會把一個double放入快取器,而只包含一個double的物件則不會
- by reference則肯定把指標放入快取器
- 使用者自定義型別的大小容易變化,因其內部實現可能改變,故不一定適合pass by value
- 某些標準程式庫實現版本中的string型別比其他版本大七倍
Tips:
- 儘量以pass-by-reference-to-const替換pass-by-value。前者通常高效且能避免切割問題
- 以上規則並不適用內建型別和STL的迭代和與函式物件,它們更適合pass-by-value
條款21:必須返回物件時,別妄想返回其reference(Don’t try to return a reference when you must return an object)
考慮有理數乘積:
class Rational {
public:
Rational(int numerator = 0, int denominator = 1);
...
private:
int n, d; //分子和分母
friend const Rational operator* (const Rational& lhs, const Rational& rhs);
};
上述程式碼中運算子以by value的方式返回值,如果要返回reference則運算子必須自己建立新Rational物件,其途徑有二:在stack或heap空間建立(反例)
//返回local物件的引用,但是local物件在離開函式時就銷燬了
const Rational& operator* (const Rational& lhs, const Rational& rhs)
{
Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
return result;
}
//難以對new建立的物件delete,尤其以下連乘的例子
const Rational& operator* (const Rational& lhs, const Rational& rhs)
{
Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
return *result;
}
//無法取得引用背後的指標
Rational w, x, y, z;
w = x * y * z; //operator*(operator*(x, y), z)
若使用static Rational避免呼叫建構函式,則會有如下問題:
//返回local物件的引用,但是local物件在離開函式時就銷燬了
const Rational& operator* (const Rational& lhs, const Rational& rhs)
{
static Rational result;
result = ... ;
return result;
}
bool operator==(const Rational& lhs, const Rational& rhs);
Rational a, b, c, d;
...
if ((a * b) == (c * d)) { ... } //==總是為true
else { ... } //,因兩側是同一個同一個stetic Rational物件的引用
必須返回新物件的函式的正確寫法為:
inline const Rational& operator* (const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
Tips:
- 絕不要返回指標或引用指向local stack物件
- 絕對不要返回引用指向heap-allocated物件
- 絕對不要在有可能同時需要多個這樣的物件時返回指標或引用指向local static物件
條款22:成員變數宣告為private(Declare data members private)
這一條似乎沒啥內容_
把成員變數宣告為private的原因如下:
- 介面的一致性:非public的成員函式只能透過函式訪問,並且可以方便的設定讀寫許可權
- 封裝
- 把成員變數隱藏在函式介面背後,可以方便地更改實現方式
- public成員變數修改後所有使用它們的客戶碼都會被破壞
- protected成員變數修改後所有使用它們的派生類都會被破壞,其並不比public更具有封裝性
Tips:
- 切記把成員變數宣告為private,這個賦予訪問資料的一致性、可細微劃分訪問控制、保證約束條件、提供充分的實現彈性
- protected並不比public更具封裝性
條款23:寧以non-member、non-friend替換member函式(Prefer non-member non-friend functions to member functions)
考慮有個類表示網頁瀏覽器:
class WebBrowser {
public:
...
void clearCache();
void clearHistory();
void removeCookies();
void clearEverything(); //執行所有清除動作
...
};
執行所有清除動作由兩個方案:
- WebBrowser 提供函式
- 由non-member函式呼叫相應的member函式
- 封裝性更高,且包裹彈性(packaging flexibility)較大,編譯相依度較低,是更好的方案
//WebBrowser 提供函式
class WebBrowser {
public:
...
void clearEverything(); //執行所有清除動作
...
};
//由non-member函式呼叫相應的member函式
void clearBrowser(WebBrowser& wb)
{
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
兩點注意事項:
- 準確地說,封裝性良好的是non-member non-friend函式,而非non-member函式
- 一個類的non-member non-friend函式可以是可以是另一個類的member
- 有些語言的函式必須定義在類內(如Eiffel,Java,C#),可以令clearBrowser成為某個工具類(utility class)的一個static member函式,而非WebBrowser的一部分或friend
- 在C++中可讓clearWebBrowser成為non-member函式且和WebBrowser位於同一名稱空間
namespace WebBrowserStuff {
Class WebBrowser { ... };
void clearBrowser(WebBrowser& wb);
...
}
名稱空間能跨越多個原始碼檔案而類不能,故可將同一名稱空間下不同功能型別的函式放在不同的標頭檔案:
標準程式庫也不是一個龐大的單一標頭檔案,而是有若干個標頭檔案,每個標頭檔案宣告std的某些功能,這樣可以使得使用者只依賴所使用的一小部分系統
//標頭檔案webbrowser.h,包含WebBrowser自身和核心功能
namespace WebBrowserStuff {
class WebBrowser { ... };
... //核心功能,如廣泛使用的non-member函式
}
//標頭檔案webbrowserbookmarks.h,
namespace WebBrowserStuff {
... ////與書籤相關的函式
}
//標頭檔案webbrowsercookies.h,
namespace WebBrowserStuff {
... //與cookie相關的函式
}
Tips:
- 寧可拿non-member non-friend函式替換member函式,以增肌封裝性、包裹彈性、功能擴充套件性
條款24:若所有引數皆需型別轉換,請為此採用non-member函式(Declare non-member functions when type conversions should apply to all parameters)
考慮有理數類:
class Rational {
public:
Rational(int numerator = 0, //建構函式刻意不為explicit
int denominator = 1); //允許int到Rational的隱式轉換
int numerator() const; //分子和分母的訪問函式
int denominator() const;
private:
...
};
若運算子*為Rational的成員函式:
class Rational {
public:
...
const Rational operator* (const Rational& rhs) const;
};
Rational oneHalf(1, 2);
Rational result = oneHalf * 2; //正確,發生了隱式型別轉換,根據int建立了Rational
result = oneHalf.operator*(2); //但如果是explicit建構函式則錯誤
result = 2 * oneHalf; //錯誤!
result = 2.operator*(oneHalf); //錯誤!重寫上式,錯誤一目瞭然
result = operator*(2, oneHalf); //錯誤!本例不存在接受int和Rational的運算子*
只有引數位於引數列內,這個引數才能隱式型別轉換
要支援混合運算,則可讓運算子*成為non-member函式:
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
member函式的反面是non-member函式,而非friend函式
從Objected-Oriented C++轉換到Template C++且Rational是一個class template時,本條款需要考慮新的問題
Tips:
- 若需要為某個函式的所有引數(包括this指標所指的那個隱喻引數)進行型別轉換,那這個函式必須是non-member
條款25:考慮寫出一個不拋異常的swap函式(Consider support for a non-throwing swap)
預設的swap
預設情況下swap動作可由標準程式庫提供的swap演算法完成:
namespace std {
template<typename T> //只要T支援copying即可實現swap
void swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
}
}
特化的swap
預設的swap涉及三個物件的複製,而pimpl手法(pointer to implementation)可避免這些複製:
置換兩個Widget物件值只需要置換其pImpl指標;而預設的swap會複製三個Widget,並且複製三個WidgetImpl物件
class Widget {
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs) //複製Widget時,就複製WidgetImpl物件
{
...
*pImpl = *(ths.pImpl);
...
}
...
private:
WidgetImpl* pImpl; //所指物件內涵Widget資料
};
將std::swap針對Widget特化可解決上述問題:
令Widget宣告public swap成員函式做真正的置換工作(採用成員函式是為了取用private pImpl,non-member函式則不行),再把std::swap特化
class Widget {
public:
...
void swap(Widget& other)
{
using std::swap; //這個宣告有必要,稍後解釋
swap(pImpl, other.pImpl); //真正做置換工作,
}
...
};
namespace std {
template<> //表示其是std::swap的全特化(total template specialization)版本
void swap<Widget>(Widget& a, Widget& b)
{
a.swap(b); //要置換WIdget就呼叫其swap成員函式
}
}
上述程式碼與STL容器有一致性,因為所有STL容器也都提供有public swap成員函式和std::特化版本(以呼叫成員函式)
若Widget和WidgetImpl都是class template,可考慮把WidgetImpl內的資料型別引數化:
template<typename T>
class WidgetImpl { ... };
template<typename T>
calss Widget { ... };
此時特化std::swap會遇到問題:
//以下程式碼企圖偏特化(partially specialize)一個function template(std::swap)
//但C++只允許對class template偏特化
//故無法透過編譯(雖然少數編譯器錯誤地透過編譯)
namespace std {
template<typename T>
void swap<Widget<T>>(Widget<T>& a, Widget<T>& b)
{ a.swap(b);}
}
//偏特化function template時,通常會新增過載版本
//但以下程式碼也不合法,因為std不能新增新的templates,這由C++彼岸準委員會決定
namespace std {
template<typename T>
void swap(Widget<T>& a, Widget<T>& b) //注意swap之後沒有<...>
{ a.swap(b); }
}
解決方案:宣告一個non-memebr swap以呼叫member swap,但不再將non-member swap宣告為std::swap的特化版本或過載版本
任何程式碼如果要置換兩個Widget物件而呼叫swap,則C++的名稱查詢法則(name lookup rules;更具體地說是argument-dependent lookup或Koeing lookup法則)會找到WidgetStuff內的Widget專屬版本
namespace WidgetStuff { //為簡化,把Widget相關功能都放入WidgetStuff名稱空間內
...
template<typename T>
class Widget { ... };
...
template<typename T>
void swap(Widget<T>& a, Widget<T>& b) //non-member swap函式,不屬於std名稱空間
{
a.swap(b);
}
}
若想要class專屬版的swap在儘可能多的語境下被呼叫,則需呀在該class所在的名稱空間內寫一個non-member版本和一個std::特化版本,故應該為該class特化std::swap
若希望呼叫T專屬版本,並且在該版本不存在的情況下呼叫std內的一般化版本,可實現如下:
C++的名稱查詢法則確保會找到global作用域或T所在的名稱空間內的任何T專屬的swap;若沒有專屬swap則using宣告使得能夠呼叫std::swap
template<typename T>
void doSomething(T& obj1, T& obj2)
{
using std::swap; //令std::swap在此函式內可用
...
swap)obj1, obj2); //為T呼叫最佳swap版本
...
}
std::swap(obj1, obj2); //錯誤的方式!強迫編譯器呼叫std::swap
使用swap的總結
swap的使用總結如下:
- 若預設的swap的效率可接受,則無需做額外的事
- 若預設的swap效率不足,則可考慮:
- 提供public swap成員函式,使其置換相應型別的兩個物件值,且絕不丟擲異常
- 在class或template所在的名稱空間內提供一個non-member swap,並呼叫上述swap成員函式
- 若正在編寫class(而非class template),則特化std::swap並使其呼叫swap成員函式
- 若呼叫swap,則需要包含using宣告式,使std::swap在函式內可見,之後不加namespace直接呼叫swap
成員版swap絕不可丟擲異常,其最好的應用是幫助class或class template提供強烈的異常安全性(exception-safety)保障
Tips:
- 當std::效率不高時,提供一個swap成員函式,並確保其不丟擲異常
- 如果提供一個member swap,則要提供一個non-member swap呼叫前者。對於class(而非template),也最好特化std::swap
- 呼叫swap時應宣告 using std:;swap,之後不帶名稱空間修飾地呼叫swap
- 為自定義型別進行std template全特化可以,但是不要再std內加入新東西`