目錄條款26:儘可能延後變數定義式的出現時間(Postpone variable definitions as long as possible)條款27:儘量少做轉型動作(Minimize casting)條款28:避免返回handles指向對象內部成分(Avoid returning “hand ...
目錄
- 條款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
- const_cast:通常將對象的常量性移除(cast away the constness)
//舊式轉型
(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(懸空的號碼牌),即所指的東西不復存在:
- boundingBox返回一個新的、暫時的Rectangle對象(權且稱temp)
- temp.upperLeft()返回指向temp內部的Point的引用
- 引用賦給pUpperLeft
- temp會被銷毀,則其內部Point析構
- 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++庫的頭文件
- 對應的定義分佈在若幹不同的頭文件內,包括
、 、 、 - 彰顯本條款適用於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都適用