September 8, 2014 · 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 記憶體等等,則針對這些設計窮舉所有的可能實作似乎也不切實際。

解法一:多重繼承

但缺點是

解法二:template

但有以下限制

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 */ }

比較:多重繼承 v.s. template

解法三:結合 template 與多重繼承

既然 template 與多重繼承各有優缺點,何不將這兩者結合起來?在這裡我們將不同的設計面相視為一個 policy,並利用 template 與多重繼承的特性將它們「組裝」到一個 class 裡頭。

Policy 定義了 class(或 class template)的 interface。實作 policy 的 class 被稱為 policy class。使用一個或多個 policies 的 class 被稱作 host class。

來看個書上的例子: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?

Enriched Policies

上面例子中,PrototypeCreator 額外定義了 GetPrototype()SetPrototype(),意味著這兩個額外的 member function 會被其 host classes 繼承,於是 user 便可以直接呼叫它們:

typedef WidgetManager<PrototypeCreator> MyWidgetManager;  
...
Widget *pPrototype = ...;  
MyWidgetManager mgr;  
mgr.SetPrototype(pPrototype);  
... use mgr ...

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>,因此上面這串程式是可以成功編譯的,卻會導致異常的行為。

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

  • LinkedIn
  • Tumblr
  • Reddit
  • Google+
  • Pinterest
  • Pocket
Comments powered by Disqus