《Effective C++》第三版-5. 實現(Implementations)

Roanapur發表於2024-05-16

目錄
  • 條款26:儘可能延後變數定義式的出現時間(Postpone variable definitions as long as possible)
  • 條款27:儘量少做轉型動作(Minimize casting)
  • 條款28:避免返回handles指向物件內部成分(Avoid returning “handles” to object internals)
  • 條款29:為“異常安全”而努力是值得的(Strive for exception-safe code)
    • 異常不安全的案例
    • 異常安全函式的保證
    • 強烈的異常安全
  • 條款30:透徹瞭解inlining的裡裡外外(Understand the ins and outs of inlining)
  • 條款31:將檔案間的編譯依存關係降至最低(Minimize compilation dependencies between files)
    • 編譯依賴的來源
    • 分離類的實現
    • 分離的關鍵與編譯依存性最小化
    • 控制代碼類和介面類
    • 代價

條款26:儘可能延後變數定義式的出現時間(Postpone variable definitions as long as possible)

應延後變數的定義,知道不得不使用該變數的前一刻為止,甚至直到能夠給他初值實參為止

當程式的控制流達到變數的定義式時,會有構造成本;當離開變數的作用域時,會有析構成本

std::string encryptPassword(const std::string& password)
{
	...
	std::string encrypted(password);  //透過copy建構函式定義並初始化
	encrypt(encrypted);
	return encrypted;
}

考慮在迴圈中使用的變數:

  • 定義於迴圈外:
    • 複製成本低於構造+析構成本時一般更高效,此時適用於效率高度敏感(performance-sensitive)的部分
    • 定義的變數作用域更大,可能降低程式的可讀性和易維護性
  • 定義於迴圈內:
    • 其他情況適用
//定義於迴圈外:1個建構函式+1個解構函式+n個賦值操作
Widget w;
for (int i = 0; i < n; ++i) {
	w = 取決於i的某個值;
	...
}
//定義於迴圈內:n個建構函式+n個解構函式
for (int i = 0; i < n; ++i) {
	Widget w(取決於i的某個值);
	...
}

Tips:

  • 儘可能延後變數定義式的出現,這樣可增加程式的清晰度並改善程式效率

條款27:儘量少做轉型動作(Minimize casting)

C++支援的轉型動作通常有三種形式:

  • 舊式轉型
    • C風格的轉型
    • 函式風格的轉型
  • 新式轉型(也稱new style或C++-style cast)
    • const_cast:通常將物件的常量性移除(cast away the constness)
      • 是唯一由此能力的C++-style運算子
    • dynamic_cast:主要用來執行安全向下轉型(safe downcasting),即決定某個物件是否歸屬繼承體系中的某個型別
      • 是唯一無法用舊式語法執行的動作
      • 也是唯一可能耗費重大執行成本的轉型動作
    • reinterpret_cast:執行低階轉型,實際動作和結果可能取決於編譯器,故不可移植
      • 如把pointer to int轉型為int,這類轉換在低階程式碼以外很少見
      • 本書只在針對原始記憶體(raw memory)寫出一個除錯用的分配器(debugging allocater)時使用,見條款50
    • static_cast:強迫隱式轉換(implicit conversion)
      • 如non-const到const,int到double,以及上述多種轉換的反向轉換,如void*指標到typed指標,pointer-to-base到pointer-to-derived
      • 無法將const轉為non-const
//舊式轉型
(T)expression;  //C風格的轉型
expression(T);  //函式風格的轉型
//新式轉型
const_cast<T>( expression );
dynamic_cast<T>( expression );
reinterpret_cast<T>( expression );
static_cast<T>( expression );

新式轉換相對舊式轉換有兩個優點:

  • 易於辨識,從而簡化定位錯誤的過程
  • 轉型動作的目標約窄化,編譯器越可能診斷出錯誤的地方

使用舊式轉型的時機:當要呼叫explicit建構函式將一個物件傳遞給一個函式

class Widget {
public:
	explicit WIdget(int size);
	...
};
doSomeWork(Wistaticdget(15));  //函式風格轉型
doSomeWork(static_cast<Widget>(15));  //C++風格轉型

任何一類轉換往往令編譯器編譯出執行期間執行的碼。

下例中pb和&d可能不相同,此時會有偏移量在執行期被施行於Derived指標上,以取得Base的指標值。此事在多重繼承中幾乎一直髮生,在單一繼承中也可能發生,且偏移量可能編譯器的不同而不同,故應避免這種用法

class Base { ... };
class Derived: public Base { ... };
Derived d;
Base* pb = & &d;  //把Derived*隱式轉換為Base*

考慮下例:許多應用框架(application frameworks)都要求派生類內的虛擬函式程式碼的第一個動作就先呼叫基類的對應函式,此處假設SpecialWindow的onResize函式要首先呼叫Window的onResize函式

class Window {
public:
	virtual void onResize() { ... }
	...
};
class SpecialWindow: public WIndow {
public:
	virtual void onResize() {
		static_cast<WIndow>(*this).onResize();  //不可行!將*this
		...  //這裡進行SpecialWindow專屬行為
	}
	...
};

上述程式碼呼叫的不是當前物件上的函式,而是轉型動作所建立的*this物件的基類成分的副本的onResize。

若Window::onResize修改了物件內容,則改動的是副本而非當前物件;若SpecialWIndow::onResize也修改物件內容,則當前物件會被改動

正確的寫法如下:

class SpecialWindow: public Window {
public:
	virtual void onResize() {
		Window::onResize();  //呼叫Window::onResize作用於*this
		...
	}
	...
};

dynamic_cast的注意事項:

  • 許多實現版本執行速度相當慢
  • 可在只有指向基類的指標或引用時為派生類物件身上執行其操作函式
  • 避免使用dynamic_cast的一般性方法有二(並非放之四海而皆準)
    • 使用容器並在其中儲存直接指向派生類物件的指標
      • 無法在同一容器記憶體儲指向不同派生類的指標,需要多個容器處理多種派生類且必須具備型別安全性(type-safe)
    • 透過基類介面處理所有派生類,即在基類內提供虛擬函式做想對派生類做的事
  • 決不能連串(cascading)dynamic_cast
//使用容器並在其中儲存直接指向派生類物件的指標
class Window { ... };
class SpecialWindow: public Window {
public: 
	void blink();  //閃爍效果
	...
};
//容器內是派生類而非基類,免去在迴圈中使用dynamic_cast把積累轉換為派生類的步驟
typedef std::vector<std::tr1::shared_ptr<SpecialWindow>> VPSW;
VPSW winPtrs;
...
for (VPSW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
	(*iter)->blink();  //這樣寫比較好,不使用dynamic_cast

//透過基類介面處理所有派生類
class Window {
public:
	virtual void blink() { }
		...
};
class SpecialWindow: public Window {
public: 
	virtual void blink() { ... }
	...
};
typedef std::vector<std::tr1::shared_ptr<Window>> VPW;
VPW winPtrs;
...
for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
	(*iter)->blink(); 
	
//連串dynamic_cast
for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
	if (SpecialWindow1 * psw1 = dynamic_cast<SpecialWindow1*>(iter->get()) { ... }; 
	else if (SpecialWindow2 * psw2 = dynamic_cast<SpecialWindow2*>(iter->get()) { ... };
	else if (SpecialWindow3 * psw3 = dynamic_cast<SpecialWindow3*>(iter->get()) { ... };
}

Tips:

  • 儘量避免撰寫,尤其是在注重效率的程式碼中避免dynamic_cast
  • 若轉型有必要,嘗試將它隱藏於某個函式背後,客戶隨後可以呼叫該函式而非在他們自己的程式碼內轉型
  • 寧可使用新式轉型也不要使用舊式轉型,前者易分辨且分類更細

條款28:避免返回handles指向物件內部成分(Avoid returning “handles” to object internals)

考慮涉及矩形的例子:

引用、指標、迭代器都是所謂的handles(號碼牌,用來取得某個物件)

class Point {
public:
	Point(int x, int y);
	...
	void setX(int newVal);
	void setY(int newVal);
	...
};

struct RectData {
	Point ulhc;
	Point lrhc;
};
class Rectangle {
public:
	...
	// 如果沒有cosnt Point&,則引用指向的內容可能變化
	// Point& upperLeft() const { return pData->ulhc; }
	// Point& lowerRight() const { return pData->lrhc; }
	// 採取這種方式可保證handle指向的資料不變
	const Point& upperLeft() const { return pData->ulhc; }
	const Point& lowerRight() const { return pData->lrhc; }
	...
private:
	std::tr1::shared_ptr<RectData> pData;
};

class Rectangle {

};

即使指向的內容不變,返回handle還是可能導致dangling handle(懸空的號碼牌),即所指的東西不復存在:

  1. boundingBox返回一個新的、暫時的Rectangle物件(權且稱temp)
  2. temp.upperLeft()返回指向temp內部的Point的引用
  3. 引用賦給pUpperLeft
  4. temp會被銷燬,則其內部Point析構
  5. pUpperLeft懸空!
class GUIObject { ... };  //考慮GUI物件的矩形外框
const Rectangle boundingBox(const GUIObject& obj);  //以by value返回矩形
GUIObject* pgo;
...
const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());  //可能懸空!

Tips:

  • 避免返回handles(包括引用、指標、迭代器)指向物件內部,則可增加封裝性並減少懸空號碼牌的可能性

條款29:為“異常安全”而努力是值得的(Strive for exception-safe code)

異常不安全的案例

class PrettyMenu {
public:
	...
  void changeBackground(std::istream& imgSrc) // 改變背景影像
	...
private:
    Mutex mutex;  //互斥器
    Image* bgImage;  //目前使用的背景圖片
    int imageChanges;  //圖片被修改的次數
};

void PrettyMenu::changeBackground(std::istream& imgSrc) {
	lock(&mutex);  //取得互斥器
	delete bgImage;  //刪除舊圖片
	++imageChanges;  //修改影像更改次數
	bgImage = new Image(imgSrc);  //安裝新的背景圖片
	unlock(&mutex);  //釋放互斥器
}

當異常被丟擲時,異常安全函式會(上述程式碼均不滿足):

  • 不洩露任何資源
    • 一旦new Image(imgSrc)異常,則unlock不會被呼叫,那互斥器將永遠鎖住
  • 不允許資料敗壞
    • 若new Image(imgSrc)異常,則bgImage就指向已被刪除的物件,imageChanges也已累加,但實際上並沒有影像成功安裝

異常安全函式的保證

異常安全函式提供以下三個保證之一:

  • 基本承諾:若異常被丟擲,程式內的任何事物仍然保持在有效狀態下
  • 強烈保證:若異常被丟擲,程式狀態不改變
    • 若函式成果,就是完全成功;若函式失敗,程式會恢復到呼叫函式之前的狀態
  • 不拋擲(nothrow)保證:承諾絕不丟擲異常,因為它們總是能完成原先承諾的功能
    • 作用於內建型別身上的所有操作都提供nothrow保證

讓changeBackground提供接近但非完全的強烈的異常安全保證可考慮以下兩點:

  • 改變PrettyMenu的bgImage成員變數的型別
    • 改用智慧指標
  • 重新排列changeBackground內的語句次序
    • 在更換影像之後再累加imageChanges
class PrettyMenu {
	...
	std::tr1::shared_ptr<Image> bgImage;
	...
};

void PrettyMenu::changeBackground(std::istream& imgSrc) {
    Lock ml(&mutex);  //將互斥器封裝在類中進行管理
    bgImage.reset(new Image(imgSrc));  //以new Image的執行結果設定bgImage內部指標
    ++imageChanges; 
}

上述程式碼刪除動作只發生在新影像建立成功之後,shared_tpr::reset函式只有在其引數(即new Image(imgSrc))成功之後才會被呼叫。delete在reset內呼叫,則未進入reset就不會delete。

但是如果Image建構函式丟擲異常,則可能輸入流(input stream)的讀取記號(read marker)已被移走,這對程式其餘部分是一種可見的狀態改變。

強烈的異常安全

copy and swap可以提供強烈的保證

  • 如果在副本的身上修改丟擲了異常,那麼原物件未改變狀態。
  • 如果在副本的身上修改未丟擲異常,那麼就將修改過的副本與原物件在不丟擲異常的操作中置換(swap)。

實際上通常是將所有隸屬物件的資料從原物件放進另一個物件內,然後賦予原物件一個指標,指向那個所謂的實現物件(即副本),其被稱為pimpl idiom:

struct PMImpl {//將bgImage和imageChanges從PrettyMenu獨立出來,封裝成一個結構體
  std::tr1::shared_ptr<Image> bgImage;
  int imageChanges
};
class PrettyMenu {
	...
private:
    std::tr1::shared_ptr<PMImpl> pImpl; //建立一個該結構
};
void PrettyMenu::changeBackground(std::istream& imgSrc) {
    using std::swap;  //見條款25
    Lock ml(&mutex);  //獲得mutex的副本資料
    std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));
    pNew->bgImage.reset(new Image(imgSrc));  //修改副本
    pNew->imageChanges++;
    swap(pImpl, pNew);  //置換資料,釋放mutex
}

上述程式碼將PMImpl定義為一個結構體而不是類

  • PrettyMenu的封裝性透過pImpl是private來保證
  • 把PMImpl定義成類不差,但不太方便
    • PMImpl可以被放在PrettyMenu中,但是要考慮打包問題(packaging)

copy and swap不保證整個函式有強烈的異常安全性:

  • 若f1和f2的異常安全性比強烈保證低,則someFunc難以具有強烈的保證
  • 若f1和f2均為強烈異常安全,f1成功時程式狀態可能改變,則當f2丟擲異常時時程式狀態和someFunc呼叫前不同
    • 問題在於連帶影響(side effect),函式若只操作區域性狀態(local state)則易有強烈保證;若對非區域性資料(non-local data)有連帶影響則難以有強烈保證
void someFunc()
{
	...  //對local狀態做副本
	f1();
	f2();
	...  //置換修改後的狀態
}

Tips:

  • 異常安全函式即使發生異常也不會洩露資源或允許任何資料結構敗壞。這樣的函式區分三種可能的保證:基本型、強烈型、不拋異常型
  • 強烈保證往往能夠以copy-and-swap實現出來,但強烈保證並非對所有函式都可實現或具備實現意義
  • 函式提供的異常安全保證通常最高只等於其所呼叫的各個函式的異常安全性中的最弱者

條款30:透徹瞭解inlining的裡裡外外(Understand the ins and outs of inlining)

過度使用inline的問題:

  • 可能增加目標碼的大小,使程式體積太大
  • 導致額外的換頁(paging)行為
  • 降低指令快取記憶體裝置的擊中率(instruction cache hit rate)
  • 效率降低

申請inline的方式:

  • 隱喻的申請:定義於class內
  • 明確的申請:使用關鍵字inline
class Person {
public:
  ...
  int age() const { return theAge; }    // 隱喻的inline申請
  ...                                   

private:
  int theAge;
};

template<typename T>                               // 明確申請inline
inline const T& std::max(const T& a, const T& b)   
{ return a < b ? b : a; }                          

inline函式一般必須在標頭檔案內

  • 大多數建置環境(building environment)在編譯期間進行inlining
    • 為了把函式呼叫替換為被呼叫的函式本體,編譯器必須知道函式長什麼樣
  • 少量例外:
    • 部分建置環境可在連線期inline
    • 少量建置環境,如基於.NET CLI(Common Language Infrastructure,公共語言基礎設定)的託管環境,能在執行時inline

templates一般在標頭檔案內

  • 編譯器需要知道一個template長什麼樣子以便需要時對它進行例項化
  • 存在少量例外,一些構建環境可以在連線期間進行template例項化

template例項化與inlining無關

  • 如果所有從template例項化出來的函式都應該inlined,則宣告該template為inline
  • 如果不需要讓template的所有例項化的函式都是inlined,就要避免宣告template為inline

inline是一個編譯器可能忽略的請求

  • 大多數編譯器拒絕把複雜的函式inlining(如包含迴圈或者遞迴的函式)
  • 所有對虛擬函式的呼叫(除非是最簡單的)也會無法inlining
    • virtual意味著等待,直到執行時再斷定哪一個函式被呼叫
    • inline意味著執行之前,用被呼叫的函式取代呼叫的位置
    • 如果編譯器不知道哪一個函式將被呼叫,則拒絕內聯這個函式本體

一個inline函式是否能真是inline,取決於使用的構建環境,主要是編譯器

  • 大多編譯器會在無法inline化時發出警告
  • 有時編譯器有意願inline,但還是可能生成一個函式本體
    • 出現函式指標時就可能生成函式本體,因為指標不能指向不存在的函式
    • 基類和派生類的構造和解構函式的層層呼叫會影響是否inline

inline函式無法隨著程式庫的升級而升級

  • 一旦inline函式需要被改變,那所有用到該inline函式的程式都需要重新編譯
  • 修改non-inline函式則只需要重新連線
    • 若程式庫使用動態連線則升級版函式可能在暗中被應用程式採納

大部分偵錯程式無法有效除錯inline函式

  • 無法在不存在的函式內設定斷點

Tips:

  • 將大部分inlining(內聯化)限制在小型、頻繁呼叫的函式上。這使得程式除錯和二進位制升級(binary upgradability)更加容易,減小程式碼膨脹的問題,增大提升程式速度的機會
  • 不要僅僅因為function templates出現在標頭檔案中,就將它宣告為inline

條款31:將檔案間的編譯依存關係降至最低(Minimize compilation dependencies between files)

編譯依賴的來源

//編譯器需要取得其實現程式碼所用到的classes string、Date、Address的定義
#include <string>
#include "date.h"
#include "address.h"

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::string theName;  // 實現細節
	Date theBirthDate;  // 實現細節
	Address theAddress;   // 實現細節
};

Person定義檔案和其包含的標頭檔案之間形成了一種編譯依存關係(compilation dependency):

  • 任何一個標頭檔案被修改,或者這些標頭檔案依賴的檔案被修改,則包含或使用Person類的檔案就必須要重新編譯
  • 這樣的連串編譯依存關係(cascading compilation dependencies)會導致不好的後果

分離類的實現

namespace std {
	class string;  // 前置宣告(錯誤!詳情見下面敘述)
} 
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;
	...
};

上述分離方法存在兩個問題:

  • string不是類,它是一個typedef(定義為basic_string)。因此,對string的前置宣告是不正確的。正確的前置宣告比較複雜,因為它涉及到了額外的模板
  • 在編譯過程中編譯器需要知道物件的大小
int main()
{
	int x;  //定義x,編譯器知道為int分配多大的空間足夠
	Person *p;  //定義Person,編譯器需要詢問Person定義式來確定多大的空間足夠
	...
}

對於Person來說,一種實現方式就是將其分成兩個類:

  • 一個只提供介面
  • 另一個實現介面,命名為PersonImpl
#include <string>  //標準程式庫元件不該被前置宣告
#include <memory>  //此乃為了tr1::shared_ptr而含入,詳後

class PersonImpl;  //Person實現類的前置宣告
class Date;  //Person介面用到的類的前置宣告
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;  //指向實現物的指標
}; 

上述程式碼中:

  • 主類Person只包含了指向類實現的指標(PersonImpl,是tr1::shared_ptr指標)
  • 該設計就是通常所說的pimpl idiom(指向實現的指標,是pointer to implementation的縮寫)

這實現和介面的真正分離:

  • Person的使用者完全脫離了datas、address、persons的實現細節,故這些類的實現可以隨意修改,但Person使用者不需要重新編譯
  • 使用者看不到Person的實現細節,不會寫出依賴這些細節的程式碼

分離的關鍵與編譯依存性最小化

這個分離的關鍵在於將定義的依存性替換為對宣告的依存性。這是編譯依存性最小化的本質:現實中讓你的標頭檔案能夠自給自足,如果達不到這個要求,依賴其他檔案中的宣告而不是定義。其他的設計都來自於這個簡單的設計策略。因此:

  • 如果使用指向物件的引用或指標能夠完成任務時就不要使用物件
    • 可以只用一個宣告來定義指向一個型別的引用和指標
    • 而定義一個型別的物件則需要使用類的定義。
  • 儘量以類的宣告替換類的定義
    • 使用類來宣告一個函式時絕不會用到這個類的定義
//使用按值傳遞引數或者按值返回(一般這樣寫不好)也不需要
class Date;  //宣告式
Date today();  
void clearAppointments(Date d);  //無需Date的定義式
  • 為宣告和定義提供不同的標頭檔案:
    • 為了符合上述準則,需要兩個標頭檔案,一個用於宣告,一個用於定義
    • 這些檔案應該保持一致,如果有個宣告被修改了,兩個地方必須同時修改
    • 庫的使用者應該總是#include一個宣告檔案,而不是自己對其進行前置宣告
#include "datefwd.h"  //標頭檔案內宣告但未定義Date類
Date today(); 
void clearAppointments(Date d);

標頭檔案datefwd.h只包含宣告,它的命名是基於標準C++庫的標頭檔案內的iostream元件宣告:

  • 對應的定義分佈在若干不同的標頭檔案內,包括
  • 彰顯本條款適用於templates和non-templates

C++中同樣提供了export關鍵字,使模板宣告從模板定義中分離出來,但支援export的編譯器很少

控制代碼類和介面類

製作控制代碼類的方法有二:

  • 將所有的函式呼叫轉移到對應的實現類中,真正的工作在後續實現類中進行
    • Person建構函式是透過使用new呼叫PersonImpl建構函式,以及Person::name函式內呼叫PersonImpl::name,這讓Person類變為控制代碼類但不改變它做的事
  • 將Person定義成特殊的抽象基類,也就是介面類,使用這種類的意圖是為派生類指定一個介面
    • 這種類沒有資料成員,沒有建構函式,有一個虛解構函式和一系列純虛擬函式
    • 類的客戶必須以Person指標或者引用來進行程式設計,因為不可能例項化包含純虛擬函式的類
//將所有的函式呼叫轉移到對應的實現類中
#include "Person.h" 
#include "PersonImpl.h"
Person::Person(const std::string& name, const Date& birthday, const Address& addr)
	: pImpl(new PersonImpl(name, birthday, addr))
{}
std::string Person::name() const
{
	return pImpl->name();
}

//將Person定義成特殊的抽象基類,也就是介面類
class Person {
public:
	virtual ~Person();
	virtual std::string name() const = 0;
	virtual std::string birthDate() const = 0;
	virtual std::string address() const = 0;
	...
};

這個類的客戶必須以Person指標或者引用來進行程式設計,因為不可能例項化包含純虛擬函式的類。(然而例項化Person的派生類卻是可能的)。

介面類只有在其介面發生變化的情況下才需要重新編譯,其它情況都不需要

介面類的客戶為這種類建立新物件的方法:

  • 一般呼叫特殊函式,其扮演派生類建構函式,稱為工廠函式或虛建構函式
  • 它們返回指向動態分配物件的指標
  • 這樣的函式在介面類中通常被宣告為static
class Person {
public:
	...
	static std::tr1::shared_ptr<Person> 
		create(const std::string& name, const Date& birthday, const Address& addr); 
	...
};
//客戶這樣使用
std::string name;
Date dateOfBirth;
Address address;
...
//建立一個物件,支援Person介面
std::tr1::shared_ptr<Person> pp(Person::create(name, dateOfBirth, address));
...
std::cout << pp->name() << " was born on " << pp->birthDate()
					<< " and now lives at " << pp->address();
...  //pp離開作用域則物件自動刪除

支援介面類介面的具象類(concrete class)必須定義,且必須呼叫真正的建構函式,這在包含了虛建構函式實現的檔案中都會發生:

//比如,Person介面類可有一個具現化派生類RealPerson
class RealPerson: public Person {
public:
	RealPerson(const std::string& name, const Date& birthday,
	const Address& addr)
	:  theName(name), theBirthDate(birthday), theAddress(addr)
	{}
	virtual ~RealPerson() {}
	std::string name() const;  //實現碼不顯示於此
	std::string birthDate() const;
	std::string address() const;
private:
	std::string theName;
	Date theBirthDate;
	Address theAddress;
};

//給出RealPerson的定義後,很容易實現Person::create
std::tr1::shared_ptr<Person> Person::create(const std::string& name,
																						const Date& birthday,
																						const Address& addr)
{
	return std::tr1::shared_ptr<Person>(new RealPerson(name, birthday, addr));
}

代價

控制代碼類:

  • 成員函式必須透過implementation指標取得物件資料,則每次訪問增加一層間接性,而每個物件消耗的記憶體數量必須增加implementation指標的大小
  • implementation指標必須初始化(在控制代碼類建構函式內),指向有動態分配得來的implementation物件

介面類:

  • 每個函式都是virtual,故每次呼叫存在簡介跳躍(indirect jump)成本
  • 介面類派生的物件必須包含vptr,其可能增加存放物件所需的記憶體(取決於該物件除了介面類之外是否還有其他virtual來源)

控制代碼類和介面類有以下缺點:

  • 會讓執行時速度變慢
  • 會為每個物件分配額外的空間

Tips:

  • 將編譯依存最小化的一般思路:依賴宣告式而非定義式,可用控制代碼類和介面類實現
  • 程式庫標頭檔案應該以完全且僅有宣告式(full and declaration-only forms)的形式存在,該方式無論是否涉及templates都適用

相關文章