如下內容是在看侯捷老師翻譯的《Modern C++ Design》書籍時,整理的code和摘要,用於不斷地溫故知新。
第一章
1. 運用 Template Template 參數實作 Policy Classes
template <template <class Created> class CreationPolicy>
// template <template <class> class CreationPolicy> <---- 也可以這樣寫
class WidgetManager : public CreationPolicy<Widget>
{...};
// 使用端
WidgetManager<OpNewCreator> MyWidgetMgr; // <--- 並未提供 Widget 模版參數
Created
是 CreationPolicy
的參數,CreationPolicy
則是 WidgetManager
的參數。 Widget
已經顯式地在 public
後寫出了,所以使用時不需要再傳一次參數給 Policy
。
儘管在模版裏寫出了 Created
,但並沒有使用到,也沒有啥貢獻,只是 CreationPolicy
的形式引數(formal argument)
從易用性角度而言,我們可以提供一些常用的 policies
,並且以“template 缺省參數”的形式提供:
template <template <class> class CreationPolicy = OpNewCreator>
class WidgetManager : ....
注意:policies
與虛函數有很大不同。policies
因爲有豐富的型別信息及靜態鏈接等特性,所以是建立「設計元素」時的本質性東西。即「設計」指定了「執行前型別如何互相作用、你能夠做什麼、不能夠做什麼」的完整規則。此外,由於編譯期纔將 host class
和其 policies
結合在一起,因此更加牢固和高效。
缺點:由於 policies
特質,不適用於動態鏈接和二進位接口。作者認爲如下的方式「難以討論、定義、實作和運用」
struct OpNewCreator {
template <class T>
static T* Create(){
return new T;
}
};
2. Poilic Class 的析構函數
許多 Policies
並無任務數據成員、純粹只是規範行爲,若給基類加入一個虛函數,會額外增加對象大小(引入一份 vptr
)。一種解法是:採用 protected
繼承或者 private
繼承(但會失去很多豐富的特性)。更輕便和有效率的解法是:定義一個 non-virtual protected
析構函數:
struct OpNewCreator {
template <class T>
static T* Create(){
return new T;
}
// 只有派生類得到的Class 纔可以摧毀這個policy對象。避免了外界通過delete 指向基類的指針的用法。
protected:
~OpNewCreator(){} // 非虛函數,無大小和速度上的開銷
};
3. 通過不完全具現化而獲得的選擇性機能
如果 class template
有一個成員函數未曾被用到,他就不會被編譯器具體實現出來,編譯器不會理他,甚至不會爲他進行語法檢查。
4. 結合 Policy Classes
當你將 policies
組合起來時,便是它們最有用的時候。
template<
class T,
template <class> class CheckingPolicy,
template <class> class ThreadingModel
>
class SmartPtr // <--- 「集成數個 policies」 的協調層
: public CheckingPolicy<T>
, public ThreadingModel<SmartPtr>{
....
T* operator->(){
typename ThreadingModel<SmartPtr>::Lock guard(*this);
CheckingPolicy<T>::Check(pointee_);
return pointee_;
}
private:
T* pointee_;
};
// 使用端
typedef SmartPtr<Widget, NoChecking, SingleThreaded> WidgetPrt;
上述同一函數中對 checkingPolicy
和 ThreadingModel
的兩個 policy classes
的運用。根據不同的 template
參數,SmartPtr::operator->
會表現出兩種不同的正交行爲,這正是 policies
的組合威力所在。
5. 以 Policy Classes 定製結構
雖然 templates
具有「無法定製 class 的結構,只能定製其行爲」的限制,但 policy-based design
支持結構方面的定製。
template <class T>
class DefaultSPStorage
{
public:
typedef T* PointerType;
typedef T& ReferenceType;
protected:
PointerType GetPointer() {return ptr_;}
void SetPointer(PointerType ptr){ ptr_ = ptr;}
private:
PointerType ptr_;
};
tempalte
<
class T,
template <class> class CheckingPolicy,
template <class> class ThredingModel,
template <class> class Storage = DefaultSPStorage // <——- 可實現指針類型的屏蔽
>
calss SmartPtr;
6. Policies 的兼容性
Policies
之間彼此轉換的各種方法中,最好又最具擴充性的方法是「以 Policy
控制 SmartPtr
對象的拷貝和初始化」,如下例子:
template<class T, template <class> class CheckingPolicy>
class SmartPtr : public CheckingPolicy<T>{
...
template<class T1, template <class> class CP1>
SmartPtr(const SmartPtr<T1, CP1>& other)
: pointee_(other.pointee_), CheckingPolicy<T>(other)
{...}
};
-
假設
ExetendWidget
派生自Widget
。當以SmartPtr<ExtendWidget, NoChecking>
初始化一個SmartPtr<Widget, NoChecking>
時,編輯器會嘗試以一個ExtendWidget*
初始化Widget*
(這會成功),然後以一個SmartPtr<Widget, NoChecking>
初始化NoChecking
。前者是派生自後者的,所以編譯器是很容易知道你想做什麼,也會正確幫你這麼做。 -
當以
SmartPtr<ExtendWidget, NoChecking>
初始化一個SmartPtr<Widget, EnforceNotNull>
時,編譯器就會嘗試將SmartPtr<ExtendWidget, NoChecking>
拿來匹配EnforceNotNull
構造函數。則依賴於EnforceNotNull
是否有對應的夠咱函數,若有,則轉換成功。或者NoChecking
有對應的轉型操作符,則也會轉換成功。除此之外,都會編譯錯誤。
這裏有一個典型的相關case:std::autop_ptr
(C++11已不推薦使用了)。
7. 將一個 Class 分解爲一堆 Policies
建議 Policy-based class design
的最困難的部分,便是如何將 class
正確地分解爲 policies
。一個準則就是「將參與 class
行爲的設計鑑別出來,並命名之」。任何處理邏輯只要有「一種以上的方法解決」,都應該被分析出來,並獨立爲 Policy
。但「過度泛化」的 host classes
會產生缺點,會有過多的 template
參數。
Policy
之間的邊界怎麼確定呢?保持正交分解很重要。不正交的分解——如果各式各樣的 policies
需要知道彼此。
template <class T>
struct IsArray{
T& ElementAt(T* ptr, size_t idx) {return ptr[idx];}
....
};
template <class> T
struct IsNotArray {};
假設還有另一個 Policy
負責析構。此時無論 SmartPtr
是否指向 Array
,都會與析構的 Policy
耦合,因爲析構的 Policy
在 IsArray
下使用 delete []
,在 IsNotArray
下使用 delete
。因此 Array
與 Destroy
不是正交的。非正交的 policies
是不完美的設計,應該儘量避免,會給 host class
和 policy class
引入額外的複雜度。
8. 總結
「設計」就是一種「選擇」,大多數時候我們的困難並不在於找不到解決方案,而是有太多方案。Policies
機制由 templates
和 多重繼承組成,Host class
的所有機能都來自 policies
,運作起來就像一個聚合無數個 Policies
的容器。
第二章
1. 編譯期 Assertions
表達式在編譯期評估所得的結果是個定值(常數),這意味着你可以用利用編譯器來做檢查。最簡單的方式稱爲 compile-time assertions
,在C和C++語言中都可以實現,它依賴一個事實:大小爲 0 的 array
是非法的。
#define STATIC_CHECK(expr) { char unnamed[(expr) ? 1: 0];} // <---- 最初版本
template <class To, class From>
To Safe_reinterpret_cast(From from)
{
STATIC_CHECK(sizeof(from) <= sizeof(To));
return reinterpret_cast<To>(from);
}
但上述實現無法提供「可讀、友好、可定製」的報錯信息,較好的解法是依賴一個名稱帶有意義的 template
。
template <bool> struct CompiledTimeError;
template <> struct CompiledTimeError<true>{}; // <--- 僅支持對 true 進行具現化
#define STATIC_CHECK(expr) (CompiledTimeError<(expr) != 0>())
爲了更進一步支「可定製化」的報錯信息,我們可以進階地修改爲:
template<bool>
struct CompiledTimeChecker{
CompiledTimeChecker(...); // <--- C++ 支持的非定量任意參數
}
template<> struct CompiledTimeChecker<false>{}; // <-- 僅對 false 進行具現化
#define STATIC_CHECK(expr, msg) \
{ \
class ERROR_##msg {}; \ // <--- local 空類
(void)sizeof(CompiledTimeChecker<(expr)>(Error_##msg)); \ // <--- Error_##msg 是類的初始化參數,sizeof最終會被調用
}
當表達式爲 false
時,編譯器找不到將 Error_##msg
轉成 CompiledTimeChecker<false>
的方法,而且會報出:Error: Cannot convert Error_xxx to CompiledTimeChecker<false>
。
2. 模版偏特化
通常在一個 class template
偏特化定義中,你只會特化某些 template
參數,而留下其他泛化參數,編譯器會嘗試找出「最匹配」的定義,雖然這個過程十分複雜和精細。
template <class Window, class Controller>
class Widget {....};
template <class ButtonArg> // <---- 支持富有創意的偏特化
class Widget<Button<ButtonArg>, MyController> {...};
但偏特化機制不能作用在「函數」身上,不論是成員函數還是非成員函數
- 可以「全特化」
class template
中的成員函數,但不能「偏特化」他們 - 不能偏特化
namespace-level(non-member)
函數,但可以藉助函數重載實現類似的效果。
template <class T, Class U>
T Func(U obj);
template <class U>
void Func<void, U>(U obj); // <---- 非法
template <class T>
T Func(Window obj); // <---- 合法,overloading 機制
3. 局部類 Local Classes
C++ 支持在函數中定義 class
,是的,沒有看錯,是在函數中定義,但有一些侷限性:
local class
不能定義static
成員變量,也不能訪問non-static
局部變量
有趣的是,local class
可以使用函數的 template
參數。當然,任何運用 local class
的手法,都可以改用「函數外的 template class
」 來完成。但 local class
可以簡化操作並提高「符號地域性」
class Interface {
public:
virtual void Fun() = 0;
};
template <class T, class P>
Interface* MakeAdapter(const T& obj, const P& arg){
class Local : public Interface { // <--- 內部類
public:
Local(const T& obj, const P& arg): obj_(obj), arg_(arg) {}
virtual void Fun() {obj_.Call(arg_);}
private:
T obj_;
P arg_;
};
return new Local(obj, arg);
}
local class
還有一個隱藏特性:它有 final
的語義。即外界不能繼承一個隱藏於函數內的 class
。
4. 常整數映射爲型別
如下是作者提出的一個思路,比較有意思,藉由「不同的 template
具現體本身就是不同的類型」。
template <int v>
struct Int2Type{
enum {value = v};
};
上述用於產生類別的數值是一個「枚舉值」,可根據編譯期計算出來的結果選用不同的函數,達到「運用常數來靜態分派」的功能。那在什麼場景下會用到這個手法呢?
- 有必要根據某個編譯期常數調用一個或不同的函數
- 有必要在編譯期實施「分派」(
dispatch
)
相對而言,執行期分派有時並非如我們預期,在編譯器層面可能會報錯,如下例子:
template <typename T, bool isPoly>
class NiftyContainer{
void DoSomething(){
T* pSomeObj = ...;
if(isPoly){ // <--- 運行時分派
T* pNewObj = pSomeObj->Clone(); // <--- 位置①
.... (多態算法)
}else{
T* pNewObj = new T(*pSomeObj); // copy 構造, 位置②
....(非多態算法)
}
}
};
如果你調用 NiftyContainer<int, false>
的 DoSomething()
,當模版參數 T
類別沒有定義成員函數 Clone()
時,上述代碼會在位置①編譯報錯。因爲編譯器總是勤奮地編譯所有的分支。
Int2Type
提供了一種明確的解法,其奧義在於「編譯器並不會去編譯一個未被使用到的 template
函數,只會做文法檢查而已」。
....
{
public:
void DoSomething(T* pObj){ DoSomething(pObj, Int2Type<isPoly>);}
private:
void DoSomething(T* pObj, Int2Type<true>){
T* pNewObj = pObj->Clone();
.... (多態算法)
}
void DoSomething(T* pObj, Int2Type<false>){
T* pNewObj = new T(*pObj);
....(非多態算法)
}
};
5. 型別對型別的映射
template
函數不支持偏特化,我們有辦法模擬實現類似的機制麼?假設我們要針對 Widget
的創建過程偏特化,因爲它的構造函數有兩個參數。
template <class T, class U>
T* Create(const U& arg){
return new T(arg);
}
// 初版方案:藉助重載機制
template <class T, class U>
T* Create(const U& arg, T /*dummy*/){
return new T(arg);
}
template <class U>
Widget* Create(const U& arg, Widget /*dummy*/){
return new Widget(arg, -1);
}
上述方案會構造未使用的對象,造成額外開銷。此處我們引入 Type2Type
:
template <class T>
struct Type2Type{
typedef T OriginalType; // <---- 沒有任何數值
};
template <class T, class U>
T* Create(const U& arg, Type2Type<T>){
return new T(arg);
}
template <class U>
Widget* Create(const U& arg, Type2Type<Widget>){
return new Widget(arg, -1);
}
// 使用端
String* pStr = Create("hello", Type2Type<String>())();
Widget* pW = Create(100, Type2Type<Widget>())();
Type2Type
參數只是用來選擇合適的「重載函數」。
6.型別選擇
在前面的 NiftyContainer
例子中,你可能會選擇 std::vector
作爲後端的存儲結構,對於多態類型,不能存儲實例,必須存儲指針;對於非多態類型,可以存儲實例(這樣效率更高)。你可能會想到根據 isPoly
參數動態決定將 ValueType
定義爲 T*
或 T
,如下:
template <class T, bool isPoly>
struct NiftyContainerValueTraits {
typedef T* valueType;
};
template <class T>
struct NiftyContainerValueTraits<T, false> {
typedef T valueType;
};
template <class T, bool isPoly>
class NiftyContainer{
...
typedef NiftyContainerValueTraits<T, isPoly> Traits;
typedef typename Traits::ValueType ValueType; // <---- 藉助 Traits 機制
};
如上實現方案,針對不同的類,都必須定義專屬的 Traits class template
。(爲什麼?不是隻針對「是否多態」進行偏特化就可以了,爲什麼這裏會說對不同的類也要定義專屬的 Traits
呢?)
Loki 裏的實現是如下機制:
template <bool flag, class T, class U>
struct Select{
typedef T Result;
};
template <class T, class U>
struct Select<false, T, U>{
typedef U Result;
};
template <class T, bool isPoly>
class NiftyContainer{
...
typedef Select<isPoly, T*, T>::Result ValueType;
};
7. 編譯期間偵測可轉換性和繼承性
對於兩個陌生的類型 T
和 U
,如何知道 U
是否繼承自 T
? 可以合併運用 sizeof
和重載函數,如下是魔法產生的樣例代碼:
template<class T, class U>
class Conversion{
typedef char Small;
class Big {char dummy[2];};
static Small Test(U);
static Big Test(...);
static T MakeT(); // not implemented
public:
enum {exists = sizeof(Test(MakeT())) == sizof(Small);}
enum {sameType = false;}
};
template <class T> // 偏特化
class Conversion<T, T>{
public:
enum {exists = 1, sameType = 1};
};
// 用戶端代碼
int main(){
using namespace std;
cout << Conversion<double, int>::exists << endl; // 1
cout << Conversion<char, char*>::exists << endl; // 0
cout << Conversion<size_t, vector<int>>::exists << endl; // 0
}
有了 Conversion
的幫助,我們很容易在編譯期判斷兩個 class
是否具有繼承關係:
#define SUPER_SUB_CLASS(T, U) \
(Conversion<const U*, const T*>::exists && \
!Conversion<const T*, conost void*>::sameType)
如果 U
是 public
繼承自 T
,或 T
和 U
是同一類別,SUPER_SUB_CLASS(T, U)
會返回 true
。爲什麼這些代碼要加上 const
修飾?原因是我們不希望因爲 const
而導致轉型失敗。
8. type_info 的一個 Wrapper
type_info
常常和 typeid
操作符一起使用,後者返回一個 reference
,指向一個 type_info
對象:
void func(Base* ptr){
if(typeid(*ptr) == typeid(Derived)){
//.....
}
}
typd_info
支持 operator==
、operator!=
,還提供了額外的兩個函數:
name()
,返回一個const char*
before()
,帶來type_info
對象的次序關係,可以藉助此接口對type_info
對象建立索引
但type_info
關閉了copy
構造函數和賦值構造函數,導致不可以存儲它,但可以存儲它的指針,因爲typeid
傳回的對象採用的是static
存儲方式,不用擔心生命週期問題。但C++並不保證每次調用typeid(int)
會傳回“指向同一個type_info
對象”的reference
。