Introduction
本章討論了 變量聲明, 變量轉換(casting), 變量返回, 異常處理, inline, 解決編譯依賴等問題. 信息量較大也涉及到了設計模式的問題, 需仔細咀嚼.
Rule 26: Postpone variable definitions as long as possible
儘量延後程序定義式的出現, 因爲考慮到如果出現 exceptions 的話, 過早定義大量變量會造成不必要的構造和析構函數時間的浪費. 例如:
string encryptPassword(const string& password)
{
string encrypted;
if (password.length() < MinimumPasswordLength){
throw logic_error("Password is too short");
}
...
return encrypted;
}
如果在中途拋出 logic_error, 就會造成 encrypted 的構造函數析構函數時間的浪費.
Remeber:
儘量延後變量定義式的出現. 這樣做可增加程序的清晰性並改善程序的效率.
Rule 27: Minimize casting
C風格舊式轉型:
(T)expression
T(expression)
C++新式轉型(cast):
const_cast<T>(expression) // 常量性轉除 cast away the constness
dynamic_cast<T>(expression) // "安全向下轉型", 將一個基類對象指針(或引用)cast到繼承類指針
reinterpret_cast<T>(expression) // 意圖執行低級轉型, 用的較少
static_cast<T>(expression) //強迫隱式轉換, non-cast轉cast, int轉double等等, 但是不能 const 轉 non-const
新式轉型更好使用, 一是易於識別, 二是目標更加明確化
Remeber:
如果可以儘量避免轉型, 特別在注重效率的代碼中避免 dynamic_casts. 最好使用c++新式轉型
Rule 28: Avoid returning “handles” to object internals
避免返回 handles 指向對象內部成分, 首先, 是爲了提高函數的封裝性.
首先考慮一個例子, 返回矩陣的頂點:
class Rectangle{
public:
…
Point& upperLeft() const { return pData->ulhc; }
…
}
對於上面這種情況, 雖然函數聲明爲const, 卻把內部private變量的引用傳出去了, 這個時候 private 變量不再 private, 可以任意修改. 目前較新的編譯器不會通過上述編譯:
main.cpp:30:10: error: binding ‘const Point’ to reference of type ‘Point&’ discards qualifiers
所以說, 有兩種寫法:
硬是要返回內部變量的 reference (破壞封裝)
class Rectangle{ public: ... Point& upperLeft() { return pData->ulhc; } ... }
返回值, 保持封裝, 安全性
class Rectangle{ public: ... Point upperLeft() const { return pData->ulhc; } ... }
Remeber:
避免返回 handles 指向對象內部成分(包括成員變量, 函數指針, 迭代器). 這樣可以提高類的封裝性.
Rule 29: Strive for exception-safe code
着一條Rule的信息量較大, 考慮到了異常處理的問題.
首先以例子爲引入, PrettyMenu 是一個用來表現背景團的GUI菜單 class, 該class期望多線程環境, 下面是 PrettyMenu
的 changeBackground
函數的一個實現:
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
lock(&mutex);
delete bgImage;
++imageChanges;
bgImage = new Image(imgSrc); // 該構造函數可能發生異常
ulock(&mutex);
}
上面這段代碼就不是個異常安全函數, 因爲一旦構造函數發生異常, 那麼會產生 資源泄漏和數據敗壞. 因爲互斥鎖已經被鎖住, bgImage指向一個刪除的對象, 並且 imageChanges也被累加.
異常安全函數(Exception safety)
在異常拋出的時候會:
- 不泄露任何資源
- 不允許數據敗壞
異常安全函數除了滿足上面兩個基本條件之外, 還有有三個級別:
- 基本承諾: 程序內的任何食物仍然保持在有效狀態下
- 強烈保證: 類似於原子性(Atomic), 如果函數成功就完全成功, 如果失敗, 則恢復到”調用之前狀態”
- 不拋出(nothrow)保證: 最高承諾, 承諾絕不拋出異常, 作用於內置類型(如 int, 指針等等) 上的所有操作都是 nothrow 的
爲了滿足基本承諾, 上面代碼可以這樣實現:
class PrettyMenu{
...
std::tr1::shared_ptr<Image> bgImage;
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock m1(&mutex);
bgImage.reset(new Image(imgSrc));
++imageChanges;
}
這裏使用了第三章提到的 RAII 用對象管理資源的思想, 使用了智能指針保證對象及時釋放. 這樣做, 就提供了基本安全保證.
但是考慮到, 可能在 Image 構造函數拋出異常的時候, 有可能輸入流(input stream) 的讀取記號(read marker)已被移走, 就是說之前的狀態已被改變(可能bgImage.reset已經發生了一部分, 其實按道理是不會發生的, 我們假裝可能吧hhh), 這樣就沒有滿足強烈保證.
要滿足強烈保證, 我們可以通過 copy-and-swap
的方式實現, 原則就是: 爲你打算修改的對象做一份副本, 然後在副本上修改, 在修改確認沒問題之後, 再與原件做 swap, 這樣保證不出錯
要這樣做的話寫法如下:
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
using std::swap;
Lock m1(&mutex);
std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl)); // 建立 copy
pNew->bgImage.reset(new Image(imgSrc)); // 修改副本
++pNew->imageChanges;
swap(pImpl, pNew); // swap
}
Remeber:
- 異常安全函數( Exception-safe functions ) 即使發生異常也不會泄漏資源或允許任何數據結構敗壞. 這樣的函數分爲三種級別(由低到高): 基本型, 強烈型, 不拋異常型
- “強烈保證”往往能夠以 copy-and-swap 實現出來, 但”強烈保證”並非對所有函數都可實現或具備實現意義
- 函數提供的”異常安全保證”通常是它所調用的各個函數的”異常安全保證”最弱者
Rule 30: Understand the ins and outs of inlining.
inline的整體觀念是”對此函數的每一個調用”都以函數本體替換之.
這樣做可能會增加你的目標碼(object code)的大小. inline 造成的代碼膨脹也可能會導致額外的換頁行爲(paging), 降低指令高速緩存裝置的擊中率(instruction cache hit rate).
對 virtual 函數 inline 都是無用的, inline在編譯期進行了替換, 而virtual則是運行期.
不應該對構造函數 和析構函數進行inline, 會使你的代碼膨脹.
Remeber:
大多數 inline 限制在小型和被頻繁調用的函數上, inline對於編譯器來說只是一個建議, 並不是強制執行的. inline也可能會造成代碼膨脹問題
Rule 31: Mnimizing compilation dependencies between files
當某個文件 #include 了另外一個 classes 的定義式, 那麼當其中任何一個文件發生修改的時候, 整個項目都需要重新編譯. 這將會耗費大量時間.
對於這種問題, 我們可以用前置聲明的方式來解決.
#include <string> // string 是一個 typedef 爲 basic_string<char>
class Date;
class Address;
class Person{
public:
Person(const std::string &name, const Date &birthday, const Address &addr);
std::string name() const;
std::string birthDate() const;
std::string address() const;
...
};
如果 Person 裏只含 Address 和 Date 的指針還好, 如果有對象聲明, 那麼編譯器必須在編譯期知道對象的大小:
int main()
{
Person p(params); // 編譯器需要知道大小
};
對於上面這種情況, 編譯器就無法知道如何分配空間, 會報錯, 對於這種問題, 有兩種解決辦法:
Handle classes
把 Person 分爲兩個 classes, 一個提供接口, 一個負責實現.
#include<string> #include<memory> class PersonImpl; class Date; class Address; class Person{ public: Person(const std::string &name, const Date &birthday, const Address &addr); std::string name() const; std::string birthDate() const; std::string address() const; ... private: std::tr1::shared_ptr<PersonImpl> pImpl; };
對象只含一個指針對象, 指向實現類, 這種設計被稱爲 pImpl idiom. 讓接口和實現相分離. 這樣做有個確定就是每個訪問都增加了一個間隔.
Interface classes
這個思想就是使用c++的 abstract base class(抽象基類) 描述 derived classes的接口. 通常不帶成員變量, 沒有構造函數, 只有一個virtual析構函數以及一組pure virtual函數
class Person{ public: virtual ~Person(); virtual std::string name() const = 0; virtual std::string birthDate() const = 0; virtual std::string address() const = 0; ... };
然後使用這個類的客戶必須以 Person 的 pointers 和 references 來撰寫應用程序. 這個時候, 還需要使用 factory(工廠)模式:
class Person{ public: ... static std::tr1::shared_ptr<Person> create(const std::string& name, const Date& birthday, const Address& addr); ... };
通常會採取上面這種工廠函數 或者 virtual 構造函數, 工廠函數返回指針(最好是智能指針), 指向分配對象. 函數通常被聲明爲 static.
使用interface classes的成本就是每個函數都是virtual, 增加了個virtual pointer的跳轉時間.
Remeber:
- 支持”編譯依存性最小化”的一般構想是: 依賴於聲明式, 不要依賴於定義式. 基於此構想的兩個手段是 Handle classes 和 Interface classes
- 程序庫頭文件應該以”完全且僅有聲明式”( full and declaration-only forms ) 的形式存在.