1. 程式人生 > 實用技巧 >《C++11 模板超程式設計 - 構建DSL》

《C++11 模板超程式設計 - 構建DSL》

C++11 模板超程式設計 - 構建DSL


MagicBowen 0.562016.09.17 21:22:10字數 3,459閱讀 3,626

C++是一門非常適合用來構建DSL(Domain Specific Language)的語言,它的多正規化特點為它提供了豐富的工具,尤其是C++提供了:

  • 一個靜態型別系統;
  • 近似於零抽象懲罰的能力(包括強大的優化器);
  • 預處理巨集,能夠以文字替換的方式操縱原始碼;
  • 一套豐富的內建符號運算子,它們可以被過載,且對過載的語義幾乎沒有任何限制;
  • 一套圖靈完備的模板計算系統(模板超程式設計),可以用於:
    • 生成新的型別和函式;
    • 在編譯期執行任何計算;
    • 提供靜態反射的能力;

結合這些武器,使得通過C++構建兼顧語法表達力及執行時效率的DSL成為了可能。可以說,模板超程式設計是上述所有武器中最為重要的,接下來我們使用模板超程式設計設計一個用於描述有限狀態機(FSM)的DSL。該設計最初來自於《C++模板超程式設計》一書,由於作者在書中所舉狀態機的例子比較晦澀,而且實現使用了更晦澀的boost mpl庫,為了讓這個設計更加易懂並且程式碼更加清晰,我對例子和程式碼進行了重新設計。

有限狀態機(FSM)是計算機程式設計中非常有用的工具,它通過抽象將紊亂的程式邏輯轉換成更易於理解的形式化的表達形式。

有限狀態機的領域模型由三個簡單的元素構成:

  • 狀態(state):FSM某一時刻總是處於一個狀態中,不同狀態決定了FSM可響應的事件型別以及響應的方式。

  • 事件(event):事件觸發FSM狀態的改變,事件可以攜帶具體資訊。

  • 轉換(transition):一個轉換標記了在某個事件的激勵下FSM從一個狀態到另一個狀態的躍遷。通常轉換還會有一個關聯動作(action),表示在狀態躍遷時進行的操作。將所有的轉換放在一起可以構成一個狀態轉換表(State Transition Table,STT)。

我們假設有一個跳舞機器人,它的狀態轉換關係如下圖:

圖可能是表示FSM最直觀的工具了,但是如果圖中出現太多的細節就會導致很凌亂,例如上圖為了簡潔就沒有標示每個轉換對應的action。為了讓FSM的表示更加形式化,我們將其裝換成如下表格的形式:

Current StateEventNext StateAction
closed open opened sayReady
opened close closed sayClosed
opened play dancing doDance
dancing stop opened sayStoped
dancing close closed sayClosed

如上,對於跳舞機器人,它有三種狀態:closed,opened,dancing;它可以接收四種事件:close,open,play,stop;它有四個action:sayReady,sayClosed,doDance,sayStoped。上表中的每一行表示了一種可以進行的轉換關係。用表格來表示FSM同樣易於理解,而且這種表示是相對形式化的,且容易通過程式碼來描述。

對於這樣一個由FSM表示的跳舞機器人,最常見的實現如下:

// Events
struct Close {};
struct Open {};
struct Play
{
    std::string name;
};
struct Stop {};

// FSM
struct DanceRobot
{
    void processEvent(const Open& event)
    {
        if(state == closed)
        {
            sayReady(event);
            state = opened;
        }
        else
        {
            reportError(event);
        }
    }

    void processEvent(const Close& event)
    {
        if(state == opened)
        {
            sayClosed(event);
            state = closed;
        }
        else if(state == dancing)
        {
            sayClosed(event);
            state = closed;
        }
        else
        {
            reportError(event);
        }
    }

    void processEvent(const Play& event)
    {
        if(state == opened)
        {
            doDance(event);
            state = dancing;
        }
        else
        {
            reportError(event);
        }
    }

    void processEvent(const Stop& event)
    {
        if(state == dancing)
        {
            sayStoped(event);
            state = opened;
        }
        else
        {
            reportError(event);
        }
    }

private:
    // Actions
    void sayReady(const Open&)
    {
        std::cout << "Robot is ready for play!" << std::endl;
    }

    void sayClosed(const Close&)
    {
        std::cout << "Robot is closed!" << std::endl;
    }

    void sayStoped(const Stop&)
    {
        std::cout << "Robot stops playing!" << std::endl;
    }

    void doDance(const Play& playInfo)
    {
        std::cout << "Robot is dancing (" << playInfo.name << ") now!" << std::endl;
    }

    template<typename Event>
    void reportError(Event& event)
    {
        std::cout << "Error: robot on state(" << state
                  << ") receives unknown event( " 
                  << typeid(event).name() << " )" << std::endl;
    }

private:
    // States
    enum
    {
        closed, opened, dancing, initial = closed

    }state{initial};
};
int main()
{
    DanceRobot robot;

    robot.processEvent(Open());
    robot.processEvent(Close());
    robot.processEvent(Open());
    robot.processEvent(Play{.name = "hip-hop"});
    robot.processEvent(Stop());
    robot.processEvent(Close());

    robot.processEvent(Stop()); // Invoke error

    return 0;
}

上面的程式碼中為了簡化只有Play事件攜帶了訊息內容。Robot通過函式過載實現了processEvent方法,用於處理不同的訊息。reportError用於在某狀態下收到不能處理的訊息時呼叫,它會打印出當前狀態以及呼叫執行時RTTI技術打印出訊息類名稱。

通過程式碼可以看到,上面的實現將整個有限狀態機的狀態關係散落在每個訊息處理函式的if-else語句中,我們必須通過仔細分析程式碼邏輯關係才能再還原出狀態機的全貌。當狀態機的狀態或者轉換關係發生變化時,我們必須非常小心地審查每個訊息處理函式,以保證修改不出錯。而且當狀態和事件變多的時候,每個函式的if-else層數將會變得更深。

如果你精通設計模式,可能會採用狀態模式改寫上面的程式碼。狀態模式為每個狀態建立一個子類,將不同狀態下的訊息處理函式分開。這樣當我們修改某一狀態的實現細節時就不會干擾到別的狀態的實現。狀態模式讓每個狀態的處理內聚在自己的狀態類裡面,讓修改變得隔離,減少了出錯的可能。但是狀態模式的問題在於將狀態拆分到多個類中,導致一個完整的FSM的實現被分割到多處,難以看到一個狀態機的全貌。我們必須在多個狀態類之間跳轉才能搞明白整個FSM的狀態關係。而且由於採用了虛擬函式,這阻止了一定可能上的編譯期優化,會造成一定的效能損失。

有經驗的C程式設計師說可以採用表驅動法來實現,這樣就可以避免那麼多的if-else或者子類。表可以將狀態機的關係內聚在一起,從而展示整個FSM的全貌。表是用程式碼表示FSM非常好的一個工具,可惜C語言的表驅動需要藉助函式指標,它和虛擬函式本質上一樣,都會導致編譯器放棄很多優化,效能都沒有第一種實現高。

那麼有沒有一種方法,讓我們可以以表來表示整個FSM,但是執行時效率又能和第一種實現相當?前面我們說了,可以利用模板超程式設計的程式碼生成能力。我們利用模板超程式設計建立一種描述FSM的DSL,讓使用者可以以表的形式描述一個FSM,然後在C++編譯期將其生成類似第一種實現的程式碼。這樣我們即得到了吻合於領域的表達力,又沒有造成任何執行時的效能損失!

接下來我們看看如何實現。

既然提到了使用表來表達,那麼我們已經有了一種熟識的編譯期表資料結構了,沒錯,就是TypeList。TypeList的每個元素表示一個轉換(transition),代表表的一行。按照前面給出的DanceRobot的表格表示,每行應該可以讓使用者定義:當前狀態,事件,目標狀態,以及對應的action。所以我們定義一個模板Row,它的引數是:int CurrentState, typename EventType, int NextState, void(Fsm::*action)(const EventType&),一旦它具現化後將表示一個transition,作為狀態轉換表的一行。

除了表之外,使用者還應該負責給出表中元素的定義,包括每個狀態、事件和action的定義。我們希望整個DSL框架和使用者的程式碼分離開,使用者在自己的類中定義state,event,action以及轉換表,然後DSL框架負責為使用者生成所有的事件處理函式processEvent

有了上面的思考後,我們通過DanceRobot展示我們構思的DSL的用法:

// Events
struct Close {};
struct Open {};
struct Play
{
    std::string name;
};
struct Stop {};

// FSM
struct DanceRobot : fsm::StateMachine<DanceRobot>
{
private:
    friend struct StateMachine<DanceRobot>;

    enum States
    {
        closed, opened, dancing, initial = closed
    };

    // actions
    void sayReady(const Open&)
    {
        std::cout << "Robot is ready for play!" << std::endl;
    }

    void sayClosed(const Close&)
    {
        std::cout << "Robot is closed!" << std::endl;
    }

    void sayStoped(const Stop&)
    {
        std::cout << "Robot stops playing!" << std::endl;
    }

    void doDance(const Play& playInfo)
    {
        std::cout << "Robot is dancing (" << playInfo.name << ") now!" << std::endl;
    }

    // table
    using R = DanceRobot;

    using TransitionTable = __type_list(
        //  +----------+----------+----------+----------------+
        //  |  current |   event  |  target  |  action        |
        //  +----------+----------+----------+----------------+
        Row <  closed  ,   Open   ,  opened  ,  &R::sayReady  >,
        //  +----------+----------+----------+----------------+
        Row <  opened  ,   Close  ,  closed  ,  &R::sayClosed >,
        Row <  opened  ,   Play   ,  dancing ,  &R::doDance   >,
        //  +----------+----------+----------+----------------+
        Row <  dancing ,   Stop   ,  opened  ,  &R::sayStoped >,
        Row <  dancing ,   Close  ,  closed  ,  &R::sayClosed >
        //  +----------+----------+----------+----------------+
    );
};

如上,我們希望客戶只用定義好Event,State,Action以及按照DSL的語法定義TransitionTable。最終所有訊息處理函式的生成全部交給DSL背後的fsm::StateMachine框架,它負責根據TransitionTable生成所有類似前面第一種實現中的processEvent函式,並且要求效能和它相當。fsm::StateMachine框架是和使用者程式碼解耦的,它獨立可複用的,使用者類通過我們之前介紹過的CRTP技術和它進行組合。

通過例子可以看到,TransitionTable的描述已經非常接近手工描述一張狀態表了,我們基本沒有給使用者帶來太多偶發複雜度,更重要的是我們完全通過編譯時程式碼生成技術來實現,沒有為使用者帶來任何執行時效率損失。

接下來我們具體看看StateMachine的實現。

template<typename Derived>
struct StateMachine
{
    template<typename Event>
    int processEvent(const Event& e)
    {
        using Dispatcher = typename details::DispatcherGenerator<typename Derived::TransitionTable, Event>::Result;

        this->state = Dispatcher::dispatch(*static_cast<Derived*>(this), this->state, e);

        return this->state;
    }

    template<typename Event>
    int onUndefined(int state, const Event& e)
    {
        std::cout << "Error: no transition on state(" << state 
                  << ") handle event( " << typeid(e).name() 
                  << " )" << std::endl;
        return state;
    }

protected:
    template< int CurrentState,
              typename EventType,
              int NextState,
              void (Derived::*action)(const EventType&) >
    struct Row
    {
        enum
        {
            current = CurrentState,
            next = NextState
        };

        using Fsm = Derived;
        using Event = EventType;

        static void execute(Fsm& fsm, const Event& e)
        {
            (fsm.*action)(e);
        }
    };

protected:
    StateMachine() : state(Derived::initial)
    {
    }

private:
    int state;
};

上面是StateMachine的程式碼實現,不要被這麼一大坨程式碼嚇住,我們一步步分析它的實現。

先來看它的建構函式:

StateMachine() : state(Derived::initial)
{
}

int state;

它的內部有一個私有成員state,用來儲存當前的狀態。它的建構函式把state初始化為Derived::initial。得益於CRTP模式,我們在父類模板中可以使用子類中的定義。StateMachine要求其子類中必須定義initial,用來指明初始狀態值。

接來下`onUndefined函式定義了當收到未定義的訊息時的預設處理方式。可以在子類中重定義這個方法,如果子類中沒有重定義則採用此預設版本。

template<typename Event>
int onUndefined(int state, const Event& e)
{
    std::cout << "Error: no transition on state(" << state 
              << ") handle event( " << typeid(e).name() 
              << " )" << std::endl;
    return state;
}

接下來內部的巢狀模板Row用於子類在表中定義一行transition。它的四個模板引數分別代表當前狀態、事件型別、目標狀態以及對應action的函式指標。注意由於採用了CRTP模式,這裡我們直接使用了子類的型別Derived來宣告函式指標型別void (Derived::*action)(const EventType&)

template< int CurrentState,
          typename EventType,
          int NextState,
          void (Derived::*action)(const EventType&) >
struct Row
{
    enum
    {
        current = CurrentState,
        next = NextState
    };

    using Fsm = Derived;
    using Event = EventType;

    static void execute(Fsm& fsm, const Event& e)
    {
        (fsm.*action)(e);
    }
};

上面在Row中通過定義execute方法,對action的呼叫進行了封裝,統一了所有action的呼叫形式。原有的每個action名稱不同,例如sayReadysayStoped...,後續可以統一通過呼叫Row::execute的方式進行使用了。藉助封裝層來統一不同方法的呼叫形式是一種非常有用的設計技巧。

最後我們來看StateMachineprocessEvent函式實現。

template<typename Event>
int processEvent(const Event& e)
{
    using Dispatcher = typename DispatcherGenerator<typename Derived::TransitionTable, Event>::Result;

    this->state = Dispatcher::dispatch(*static_cast<Derived*>(this), this->state, e);

    return this->state;
}

該函式是一個模板方法,它的入參是待處理的訊息,為了支援每種訊息,將訊息的型別定義為泛型。為了方便客戶獲取轉換後的目標狀態,函式結束時返回最新的狀態。我們期望它對於任一種合法的入參訊息型別,可以自動生成它的處理邏輯。

例如對於DanceRobot的Close訊息,我們希望它可以自動生成如下程式碼:

int processEvent(const Close& event)
{
    if(state == opened)
    {
        sayClosed(event);
        state = closed;
    }
    else if(state == dancing)
    {
        sayClosed(event);
        state = closed;
    }
    else
    {
        reportError(event);
    }

    return this->state;
}

而這所有神奇的事情,都是通過如下兩句程式碼完成的:

using Dispatcher = typename DispatcherGenerator<typename Derived::TransitionTable, Event>::Result;

this->state = Dispatcher::dispatch(*static_cast<Derived*>(this), this->state, e);

上面第一句,我們通過把狀態表Derived::TransitionTable和當前事件型別交給DispatcherGenerator,通過它得到了Dispatcher型別。從第二句中我們知道Dispatcher型別必須有一個靜態方法dispatch,它接收當前狀態和事件物件,然後完成所有的處理邏輯。

所以一切的關鍵都在於DispatcherGenerator<typename Derived::TransitionTable, Event>::Result所做的型別生成。它能夠根據狀態轉化表以及當前型別,生成正確的處理邏輯。那麼DispatcherGenerator怎麼實現呢?

我們再來看看如下DanceRobot的Close訊息處理函式的實現:

if(state == opened)
{
    sayClosed(event);
    state = closed;
}
else if(state == dancing)
{
    sayClosed(event);
    state = closed;
}
else
{
    reportError(event);
}

我們發現,它的實現是形式化的。就是根據當前訊息型別Close,在狀態轉換表Derived::TransitionTable中找到所有可以處理它的行:

//  +----------+----------+----------+----------------+
//  |  current |   event  |  target  |  action        |
//  +----------+----------+----------+----------------+
Row <  opened  ,   Close  ,  closed  ,  &R::sayClosed >,
Row <  dancing ,   Close  ,  closed  ,  &R::sayClosed >

TypeList已經有__filter元函式,它根據一個指定的規則,將TypeList中所有滿足條件的元素過濾出來,返回由所有滿足條件的元素組成的TypeList。接下來要做的是用過濾出來的行,遞迴地完成如下模式的if-else結構:

template<typename Transition, typename Next>
struct EventDispatcher
{
    using Fsm = typename Transition::Fsm;
    using Event = typename Transition::Event;

    static int dispatch(Fsm& fsm, int state, const Event& e)
    {
        if(state == Transition::current)
        {
            Transition::execute(fsm, e);
            return Transition::next;
        }
        else
        {
            return Next::dispatch(fsm, state, e);
        }
    }
};

最後的一個else中呼叫未定義訊息的處理函式:

struct DefaultDispatcher
{
    template<typename Fsm, typename Event>
    static int dispatch(Fsm& fsm, int state, const Event& e)
    {
        return fsm.onUndefined(state, e);
    }
};

到此,基本的思路清楚了,我們把上述生成processEvent函式的這一切串起來。

template<typename Event, typename Transition>
struct EventMatcher
{
    using Result = __is_eq(Event, typename Transition::Event);
};

template<typename Table, typename Event>
struct DispatcherGenerator
{
private:
    template<typename Transition>
    using TransitionMatcher = typename EventMatcher<Event, Transition>::Result;

    using MatchedTransitions = __filter(Table, TransitionMatcher);

public:
    using Result = __fold(MatchedTransitions, DefaultDispatcher, EventDispatcher);
};

上面我們首先使用__filter(Table, TransitionMatcher)在表中過濾出滿足條件的所有transition,將過濾出來的TypeList交給MatchedTransitions儲存。TransitionMatcher是過濾條件,它呼叫了EventMatcher去匹配和DispatcherGenerator入參中Event相同的Transition::Event

最後,我們對過濾出來的列表MatchedTransitions呼叫__fold元函式,將其中每個transition按照EventDispatcher的模式去遞迴摺疊,摺疊的預設引數為DefaultDispatcher。如此我們按照過濾出來的錶行,自動生成了遞迴的if-else結構,該結構存在於返回值型別的靜態函式dispatch中。

這就是整個DSL背後的程式碼,該程式碼的核心在於利用模板超程式設計遞迴地生成了每種訊息處理函式中形式化的if-else巢狀程式碼結構。由於模板超程式設計的所有計算在編譯期,模板中出現的封裝函式在編譯期都可以被內聯,所以最終生成的二進位制程式碼和最開始我們手寫的是基本一致的。

如果對本例的完整程式碼該興趣,可以檢視TLP庫中的原始碼,位置在"tlp/sample/fsm"中。


後記

返回 C++11模板超程式設計 - 目錄