/ C++

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 的實作:NoCheckingEnforceNotNull。前者不對 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>,其中 ExtendedWidgetWidget 的 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(例子?)。