Policy-Based Class Design
《Modern C++ Design》第一章讀書筆記
問題
軟體設計中,常會有所謂的「constraints」(譬如說,你想限制某個 class 無法建立兩個或以上的 object)。理想中,應該要在 compile time 達成大多數的 constraints。
而要將所有的 constraints 一併實作在所謂的 do-it-all interface 則缺乏彈性。譬如說,一個徹底封裝 threading 實作的 thread-safe singleton 到了另一個特殊且不可移植的 threading 環境就不再有效。
如果將不同的設計考量個別實作成 class 呢?以 smart pointer 為例,假如我們的設計考量包含 single/multiple thread、是否允許 null pointer、如何 allocate/deallocate 記憶體等等,則針對這些設計窮舉所有的可能實作似乎也不切實際。
解法一:多重繼承
但缺點是
- 沒有所謂的罐裝解法(boilerplate code)來有效組合多重繼承的 components,得自己謹慎協調繼承而來的 class 行為。
- base class 缺乏(或無法取得)sub-classes 的型別資訊。
- 需要透過 virtual inheritance 以令 base classes 操作共有的 state(個人理解是 data members),設計上較複雜。
解法二:template
但有以下限制
- 無法特化(specialize)class 的結構,只能特化其行為(member function 的實作)。(註:這裡其實有點混淆,實際上特化 class 時修改其 members 是符合 C++ 語法的。但若是只想修改一部分的 class 結構,就得重複寫其它所有的部分。可以參考作者針對這個問題的回覆。)
- 無法偏特化(partially specialize)member functions。我寫了一個測試如下:
template <typename X, typename Y>
class MyClass
{
public:
void foo(void) { /* do something */ };
template <typename P, typename Q>
void bar(P p, Q q) { /* do something */ };
};
// 無法特化被偏特化 class 的 member function
// template <typename X>
// void MyClass<X, X>::foo(void)
// { /* do something */ }
// 可以特化被全特化 class 的 member function
template <>
void MyClass<int, int>::foo(void)
{ /* do something */ }
// 無法偏特化被全特化 class 的 template member function
// template <>
// template <typename P>
// void MyClass<int, int>::bar<P, float>(P p, float q)
// { /* do something */ }
// 可以特化被全特化 class 的 template member function
template <>
template <>
void MyClass<int, int>::bar<bool, float>(bool p, float q)
{ /* do something */ }
- library 作者無法提供多個 template member function 的預設實作。
比較:多重繼承 v.s. template
- 多重繼承欠缺通用的 class 組合機制,template 則十分豐富(個人理解:可以在 template class 定義不同 class 的溝通方式,class 只要符合「公約」就行)。
- 多重繼承缺乏被組合型別的資訊,template 則能夠輕易從 template parameter 取得這個資訊。
- template 有著無法偏特化 member function 的限制,相較於此多重繼承較為彈性(really?)。
- template 的 member function 只能有一個預設實作,但可以有無限多個 base classes。
解法三:結合 template 與多重繼承
既然 template 與多重繼承各有優缺點,何不將這兩者結合起來?在這裡我們將不同的設計面相視為一個 policy,並利用 template 與多重繼承的特性將它們「組裝」到一個 class 裡頭。
Policy 定義了 class(或 class template)的 interface。實作 policy 的 class 被稱為 policy class。使用一個或多個 policies 的 class 被稱作 host class。
- Policy v.s. Traits:後者著重於型別,而前者著重於行為(可以參考這篇)。
- Policy v.s. Strategy:後者是為了在執行期能夠抽換,前者在編譯期即決定。
- Policy 是 syntax-oriented,而非 signature-oriented。只要語法構造合法,詳細的實作方式十分自由。
來看個書上的例子:Creator policy 規定了一個接受型別 T
的 template class 必須提供 Create()
member function,其不接受任何 argument,並回傳一個 T *
。
以下為此 policy 的實作:
template <class T>
struct OpNewCreator
{
static T *Create()
{
return new T;
}
};
template <class T>
struct MallocCreator
{
static T *Create()
{
void *buf = std::malloc(sizeof(T));
if (!buf) return 0;
return new(buf) T;
}
};
template <class T>
struct PrototypeCreator
{
PrototypeCreator(T *pObj = 0)
: pPrototype_(pObj)
{}
T *Create()
{
return pPrototype_ ? pPrototype_->Clone() : 0;
}
T *GetPrototype() { return pPrototype_; }
void SetPrototype(T *pObj) { pPrototype_ = pObj; }
private:
T *pPrototype_;
};
接著,假設現在有個 host class WidgetManager
需要利用 Creator policy 來建立 objects:
// Library code
template <template <class Created> class CreationPolicy>
class WidgetManager : public CreationPolicy<Widget>
{ ... };
client 就可以根據需求決定要採用哪個 policy 的實作:
// Application code
typedef WidgetManager<OpNewCreator> MyWidgetMgr;
自問自答:為何不用 composition 而用 inheritance?
- 若是 policy 本身不具有 data member,利用 composition 的方式編譯器會為 policy object「填充」多餘的 bytes;但若是使用 inheritance,policy 本身並不會佔去任何空間。
- host class 其實就是想要組合 policy 的 public methods 作為自身的 public methods。若是使用 composition 就需要自己手動一個一個 delegate 給 policy object 來操作。
- 喪失了 enriched policy 的能力,可以看下一節。
Enriched Policies
上面例子中,PrototypeCreator
額外定義了 GetPrototype()
與 SetPrototype()
,意味著這兩個額外的 member function 會被其 host classes 繼承,於是 user 便可以直接呼叫它們:
typedef WidgetManager<PrototypeCreator> MyWidgetManager;
...
Widget *pPrototype = ...;
MyWidgetManager mgr;
mgr.SetPrototype(pPrototype);
... use mgr ...
- 如果今天抽換
MyWidgetManager
內部採用的 policy,而喪失了 prototype 的功能,compiler 也能夠明確指出錯誤的地方(找不到MyWidgetManager::SetPrototype()
)。 - 從結果而論,policies 也給了 user 自行為 host class 加入新功能的能力。
Optional Functionality
另一方面,host class 也可以定義 optional 的功能:
// Library code
template <template <class Created> class CreationPolicy>
class WidgetManager : public CreationPolicy<Widget>
{
...
void SwitchPrototype(Widget *pNewPrototype)
{
CreationPolicy<Widget> &myPolicy = *this;
delete myPolicy.GetPrototype();
myPolicy.SetPrototype(pNewPrototype);
}
};
若是 policy 不支援 SetPrototype()
,只要不呼叫 SwitchPrototype()
就不會發生編譯錯誤。
Destructors of Policy Classes
typedef WidgetManager<PrototypeCreator> MyWidgetManager;
...
MyWidgetManager wm;
PrototypeCreater<Widget> *pCreator = &wm; // dubious, but legal
delete pCreator; // compiles fine, but has undefined behavior
... use mgr ...
由於 MyWidgetManager
public 繼承自 PrototypeCreater<Widget>
,因此上面這串程式是可以成功編譯的,卻會導致異常的行為。
- 若是為 policy 定義 virtual destructor 會造成額外的 overhead。
- 可以改成使用 private 繼承,但也喪失了讓 user 透過 policy class 增加 host class 功能的能力。
- 最簡易的方法是將 policy 的 destructor 宣告為 non-virtual protected,以避免直接建立 policy 的 instance。
Compatibility of Policies
假定現在我們有個 host class SmartPtr
,其根據 Check policy 來檢查存在內部的指標是否為 Null:
template
<
class T,
template <class> class CheckingPolicy
>
class SmartPtr;
假定現在有兩個 Check policy 的實作:NoChecking
與 EnforceNotNull
。前者不對 pointer 做任何檢查,後者則會在指標為 Null 時拋出 exception:
template <class T> struct NoChecking
{
static void Check(T *) {}
};
template <class T> struct EnforceNotNull
{
class NullPointerException : public std::exception { ... };
static void Check(T *ptr)
{
if (!ptr) throw NullPointerException();
}
};
我們想要建立 SmartPtr
之間的轉換規則(即,定義不同的 SmartPtr
之間是否可以相互轉換):
template
<
class T,
template <class> class CheckingPolicy
>
class SmartPtr : public CheckingPolicy<T>
{
public:
...
template
<
class T1,
template <class> class CP1
>
SmartPtr(const SmartPtr<T1, CP1> &other)
: pointee_(other.Get()), CheckingPolicy<T>(other)
{ ... }
inline T *Get(void) const
{
return pointee_;
}
...
private:
T *pointee_;
};
可以看到 SmartPtr
定義了一個 template copy constructor,可以接受所有可能的 SmartPtr
實作。有趣的地方在它的 initialization list 中,直接使用傳入的 SmartPtr
object 來初始化 CheckingPolicy<T>
。
假定現在以 SmartPtr<ExtendedWidget, NoChecking>
初始化 SmartPtr<Widget, NoChecking>
,其中 ExtendedWidget
為 Widget
的 sub-class。因此我們將以 ExtendedWidget *
(other.Get()
的回傳型別)初始化 Widget *
(pointee_
),並以 SmartPtr<ExtendedWidget, NoChecking>
初始化 NoChecking<Widget>
(註:書上寫的是「以 SmartPtr<Widget, NoChecking>
初始化 NoChecking<Widget>
,而因為前者衍生自後者,所以可以直接進行轉換」,但實際上並非如此)。
前者的轉換顯然是可行的。為了實現後者,需要先在所有實作 Check policy 的 classes 中實作 template copy constructor(這部份的實作方式是從書上的完整原始碼「Loki」挖出來的):
template <class T> struct NoChecking
{
...
template <class T1>
NoChecking(const NoChecking<T1> &) {}
...
};
template <class T> struct EnforceNotNull
{
...
template <class T1>
EnforceNotNull(const EnforceNotNull<T1> &) {}
...
};
因為 SmartPtr<ExtendedWidget, NoChecking>
衍生自 NoChecking<ExtendedWidget>
,所以實際上會匹配到它的 template copy constructor,使這段程式碼能成功通過編譯。
來看看另一種情況:由於 EnforceNotNull
的限制比起 NoChecking
要強,因此以 SmartPtr<ExtendedWidget, NoChecking>
初始化 SmartPtr<Widget, EnforceNotNull>
或許是個合理的轉換。也就是說,我們將在 initialization list 以 SmartPtr<ExtendedWidget, NoChecking>
初始化 EnforceNotNull<Widget>
。
在這種情況下,若是 EnforceNotNull
定義了接受 NoChecking
的 constructor,或者 NoChecking
定義了轉換成 EnforceNotNull
的 cast operator,就能夠成功通過編譯:
template <class T> struct EnforceNotNull
{
...
template <class T1>
EnforceNotNull(const NoChecking<T1> &) {}
...
};
這意味著我們能夠直接在 policy 層級控制 host classes 之間的轉換規則。
Decomposing a Class into Policies
- 在將一個 class 分解成 policies 時,最重要的一點是 policies 之間要 orthogonal,也就是彼此之間相互獨立。
- 若是必須使用 non-orthogonal policies,可以藉由將一個 policy class 作為另外一個 policy class 的 template function 的參數,以盡可能減少 dependency(例子?)。