Thursday, April 17, 2014

Functor, Ch5, Modern C++ Design

從這章開始,這本書使用 template 等技巧實作了 Loki library 的主要元件,也就是 design pattern 的功能。這章 Functor 的設計,是要讓 library 使用者快速套用 Command Pattern。

Command Pattern

這是 Command Pattern 的架構,其實在很多跟 UI 有關的 Framework 都使用了這樣的設計。
使用的時候,client 自訂 ConcreteCommand 要呼叫的 Receiver::Action 和參數,然後把這個物件傳給 Invoker,這樣就能在需要的時間點,讓 Invoker 執行該做的事。

因為通常是 Receiver 真正做事, Command & ConcreteCommand 只存參數等狀態,所以 Command & ConcreteCommand 常常只是 forwarding,於是這章打算把 forwarding command 包裝起來,不用每次都重複寫。包裝起來需要做到下面的功能:
  1. Create 的時候,要能設定 Receiver, action, 還有 action 的參數。
    1. Command::Execute() 可能有不同的 return type, parameter type,每一組 prototype 都是一個 class,所以要用 class template 來做。
    2. Create 的時候,可以給已知的參數,這樣 prototype 就會改變。
    3. 可以產生 MacroCommand,可以一次執行多的 ConcreteCommand.
  2. 可以被 Invoker 執行,並且呼叫正確的 action.
  3. C++ 的 callable entity 都要能使用。
    1. Function pointer
    2. Functor (class with operator())
    3. Pointer to member function
  4. 因為 Command + Execute() 本身就跟 functor 結構一樣,而且這邊設計的萬能 Command 需要同時能夠呼叫 functor 和其他 Command,就統一使用 operator() 代替 Execute,並且把 class 命名為 Functor。(要自動判斷呼叫 operator() 還是 Execute(),應該很難設計)

    Functor

    整理一下上面的需求,可以得到下面的功能列表。
    1. Functor 的 prototype (return type + parameter type) 是 template 分類的方式。
    2. 因為 parameter type 長度不固定,用 Typelist 來處理。
    3. Constructor 有三種,分別負責記住 function pointer, functor, pointer to member function.
    4. Overload operator() 來執行 constructor 給的參數。
    5. 內部處理不同 constructor 的資料,因為資料不一樣大,執行方式也不同,用 private class 繼承。
    假設 Functor 最多吃兩個參數,一開始寫出來的 code 大概像這樣,可以滿足幾個重點。第一,只要 prototype 固定, Functor 就是個完整的 type,這對 library 設計有很有用。第二,Typelist 可以處理不同長度的 prototype。第三,function pointer 和 functor 都能用,只要 constructor 給參數。第四,operator() 有不同長度的版本,利用 template 沒用到就沒實體化的特性。第五,因為 functor 等資料大小不同,用 private class 動態產生不同大小或型別的容器來存。

    template<typename RType, class TL>
    class Functor
    {
        typedef typename IndexOf<TL, 0>::Result P1;
        typedef typename IndexOf<TL, 1>::Result P2;
    public:
        template<typename Ftor>
        Functor(Ftor ftor)
        {
            m_pImpl = new FunctorImpl<RType, TL, Ftor>(ftor);
        }
        RType operator()()
        {
            return (*m_pImpl)();
        }
        RType operator()(P1 p1)
        {
            return (*m_pImpl)(p1);
        }
        RType operator()(P1 p1, P2 p2)
        {
            return (*m_pImpl)(p1, p2);
        }
    
    private:
        template<typename RType2, class TL2>
        class BaseImpl
        {
            …
        };
    
        template<typename RType2, class TL2, typename Ftor>
        class FunctorImpl : public BaseImpl<RType2, TL2>
        {
            ...
        };
    
        BaseImpl<RType, TL>* m_pImpl;
    };
    

    在實作 BaseImpl 的時候,會想跟 Functor 本身一樣提供不同長度的 operator(),並且給子類別 virtual 實作,但是這樣不行。BaseImpl::operator() 如果是 virtual,就一定要在 template 裡面實體化,因為 virtual 沒辦法事先判斷有沒有用到。如果又同時 overload,就會讓所有版本實體化,造成參數數量錯誤的 compile error。

    所以實作的方式,是用 template specialization 根據參數長度,讓每個長度的 operator() 都放在分別的 BaseImpl 裡面,這樣就可以宣告為 virtual。然後實作的 FunctorImpl 再 overload 所有長度的 operator(),符合長度的那個是繼承的 virtual 一定會實體化。其他的長度不是 virtual,不會實體化可以 compile,如果傳錯參數長度,實體化的時候也會 compile error。

    template<typename RType2, class TL2> class BaseImpl;
    template<typename R>
    class BaseImpl<R, TYPELIST_0()>
    {
    public:
        virtual R operator()() = 0;
        virtual ~BaseImpl() {}
    };
    template<typename R, typename P1>
    class BaseImpl<R, TYPELIST_1(P1)>
    {
    public:
        virtual R operator()(P1 p1) = 0;
        virtual ~BaseImpl() {}
    };
    template<typename R, typename P1, typename P2>
    class BaseImpl<R, TYPELIST_2(P1, P2)>
    {
    public:
        virtual R operator()(P1 p1, P2 p2) = 0;
        virtual ~BaseImpl() {}
    };
    
    template<typename RType2, class TL2, typename Ftor>
    class FunctorImpl : public BaseImpl<RType2, TL2>
    {
    public:
        FunctorImpl(Ftor ftor) : m_ftor(ftor) {}
        RType2 operator()()
        {
            return m_ftor();
        }
        RType2 operator()(P1 p1)
        {
            return m_ftor(p1);
        }
        RType2 operator()(P1 p1, P2 p2)
        {
            return m_ftor(p1, p2);
        }
    
    private:
        Ftor m_ftor;
    };
    

    這段是很巧妙的設計,靠著 template specialization 可以讓 virtual function 選擇性的實體化。

    Pointers to Member Function


    上面的實作,可以同時支援 pointer to function & functor。根據上面的實作,寫出 pointer to member function 是很容易的,只要在 constructor 提供兩個參數,pointer to object & pointer to member function 就可以了。
    public:
        template<class ObjPtr, class MemberPtr>
        Functor(ObjPtr objPtr, MemberPtr memberPtr)
        {
            m_pImpl = new PointerToMemberImpl<RType, TL, ObjPtr, MemberPtr>(objPtr, memberPtr);
        }
    …
    private:
        template<typename RType2, class TL2, typename ObjPtr, typename MemberPtr>
        class PointerToMemberImpl : public BaseImpl<RType2, TL2>
        {
        public:
            PointerToMemberImpl(ObjPtr objPtr, MemberPtr memberPtr) :
                m_objPtr(objPtr),
                m_memberPtr(memberPtr)
            {}
            RType2 operator()()
            {
                return (m_objPtr->*m_memberPtr)();
            }
            RType2 operator()(P1 p1)
            {
                return (m_objPtr->*m_memberPtr)(p1);
            }
            RType2 operator()(P1 p1, P2 p2)
            {
                return (m_objPtr->*m_memberPtr)(p1, p2);
            }
    
        private:
            ObjPtr m_objPtr;
            MemberPtr m_memberPtr;
        };
    


    注意 Functor constructor 的第一個參數是 pointer to object,語意可以明確表示,建構 Functor 的時候和執行的時候,都是用同一個 object。雖然 pointer to member function 比較晚介紹,但是可能是最常用的,因為 C++ 通常 member function 佔多數。

    Binding and Chaining Requests

    Functor (也就是 command pattern)有兩個常見的特殊應用,第一個是 binding,create 的同時給部份參數特定的值,但是剩餘的參數執行時才給。如此一來,執行時的 prototype 會改變。最常見的特例,就是 command pattern 建立 command 的同時,寫好幾個 setXXX() 把所有參數指定,使得執行變成 void execute(void)。

    Chaining 則是把好幾個 Functor 串成一個,也就是 command pattern 的 macro command。要注意 chaining 通常是同樣的 prototype 才能組合,這樣參數才能一致,return 也就可以選擇某一個。最常見的 chaining 通常也是 void execute(void) 這組 prototype,沒有參數和 return 的模糊。

    這兩個實作都很直接使用 template。

    No comments: