目錄條款17:讓介面容易被正確使用,不易被誤用(Make interfaces easy to use correctly and hard to use incorrectly)限制類型和值規定能做和不能做的事提供行為一致的介面條款19:設計class猶如設計type(Treat class de ...
目錄
- 條款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)
條款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內加入新東西`