C++是一門非常適合用來構(gòu)建DSL(Domain Specific Language)的語言,它的多范式特點為它提供了豐富的工具,尤其是C++提供了:
- 一個靜態(tài)類型系統(tǒng);
- 近似于零抽象懲罰的能力(包括強大的優(yōu)化器);
- 預(yù)處理宏,能夠以文本替換的方式操縱源代碼;
- 一套豐富的內(nèi)建符號運算符,它們可以被重載,且對重載的語義幾乎沒有任何限制;
- 一套圖靈完備的模板計算系統(tǒng)(模板元編程),可以用于:
- 生成新的類型和函數(shù);
- 在編譯期執(zhí)行任何計算;
- 提供靜態(tài)反射的能力;
結(jié)合這些武器,使得通過C++構(gòu)建兼顧語法表達力及運行時效率的DSL成為了可能。可以說,模板元編程是上述所有武器中最為重要的,接下來我們使用模板元編程設(shè)計一個用于描述有限狀態(tài)機(FSM)的DSL。該設(shè)計最初來自于《C++模板元編程》一書,由于作者在書中所舉狀態(tài)機的例子比較晦澀,而且實現(xiàn)使用了更晦澀的boost mpl庫,為了讓這個設(shè)計更加易懂并且代碼更加清晰,我對例子和代碼進行了重新設(shè)計。
有限狀態(tài)機(FSM)是計算機編程中非常有用的工具,它通過抽象將紊亂的程序邏輯轉(zhuǎn)換成更易于理解的形式化的表達形式。
有限狀態(tài)機的領(lǐng)域模型由三個簡單的元素構(gòu)成:
狀態(tài)(state):FSM某一時刻總是處于一個狀態(tài)中,不同狀態(tài)決定了FSM可響應(yīng)的事件類型以及響應(yīng)的方式。
事件(event):事件觸發(fā)FSM狀態(tài)的改變,事件可以攜帶具體信息。
轉(zhuǎn)換(transition):一個轉(zhuǎn)換標(biāo)記了在某個事件的激勵下FSM從一個狀態(tài)到另一個狀態(tài)的躍遷。通常轉(zhuǎn)換還會有一個關(guān)聯(lián)動作(action),表示在狀態(tài)躍遷時進行的操作。將所有的轉(zhuǎn)換放在一起可以構(gòu)成一個狀態(tài)轉(zhuǎn)換表(State Transition Table,STT)。
我們假設(shè)有一個跳舞機器人,它的狀態(tài)轉(zhuǎn)換關(guān)系如下圖:
圖可能是表示FSM最直觀的工具了,但是如果圖中出現(xiàn)太多的細節(jié)就會導(dǎo)致很凌亂,例如上圖為了簡潔就沒有標(biāo)示每個轉(zhuǎn)換對應(yīng)的action。為了讓FSM的表示更加形式化,我們將其裝換成如下表格的形式:
Current State | Event | Next State | Action |
---|---|---|---|
closed | open | opened | sayReady |
opened | close | closed | sayClosed |
opened | play | dancing | doDance |
dancing | stop | opened | sayStoped |
dancing | close | closed | sayClosed |
如上,對于跳舞機器人,它有三種狀態(tài):closed,opened,dancing;它可以接收四種事件:close,open,play,stop;它有四個action:sayReady,sayClosed,doDance,sayStoped。上表中的每一行表示了一種可以進行的轉(zhuǎn)換關(guān)系。用表格來表示FSM同樣易于理解,而且這種表示是相對形式化的,且容易通過代碼來描述。
對于這樣一個由FSM表示的跳舞機器人,最常見的實現(xiàn)如下:
// 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
事件攜帶了消息內(nèi)容。Robot通過函數(shù)重載實現(xiàn)了processEvent
方法,用于處理不同的消息。reportError
用于在某狀態(tài)下收到不能處理的消息時調(diào)用,它會打印出當(dāng)前狀態(tài)以及調(diào)用運行時RTTI技術(shù)打印出消息類名稱。
通過代碼可以看到,上面的實現(xiàn)將整個有限狀態(tài)機的狀態(tài)關(guān)系散落在每個消息處理函數(shù)的if-else
語句中,我們必須通過仔細分析代碼邏輯關(guān)系才能再還原出狀態(tài)機的全貌。當(dāng)狀態(tài)機的狀態(tài)或者轉(zhuǎn)換關(guān)系發(fā)生變化時,我們必須非常小心地審查每個消息處理函數(shù),以保證修改不出錯。而且當(dāng)狀態(tài)和事件變多的時候,每個函數(shù)的if-else
層數(shù)將會變得更深。
如果你精通設(shè)計模式,可能會采用狀態(tài)模式改寫上面的代碼。狀態(tài)模式為每個狀態(tài)建立一個子類,將不同狀態(tài)下的消息處理函數(shù)分開。這樣當(dāng)我們修改某一狀態(tài)的實現(xiàn)細節(jié)時就不會干擾到別的狀態(tài)的實現(xiàn)。狀態(tài)模式讓每個狀態(tài)的處理內(nèi)聚在自己的狀態(tài)類里面,讓修改變得隔離,減少了出錯的可能。但是狀態(tài)模式的問題在于將狀態(tài)拆分到多個類中,導(dǎo)致一個完整的FSM的實現(xiàn)被分割到多處,難以看到一個狀態(tài)機的全貌。我們必須在多個狀態(tài)類之間跳轉(zhuǎn)才能搞明白整個FSM的狀態(tài)關(guān)系。而且由于采用了虛函數(shù),這阻止了一定可能上的編譯期優(yōu)化,會造成一定的性能損失。
有經(jīng)驗的C程序員說可以采用表驅(qū)動法來實現(xiàn),這樣就可以避免那么多的if-else
或者子類。表可以將狀態(tài)機的關(guān)系內(nèi)聚在一起,從而展示整個FSM的全貌。表是用代碼表示FSM非常好的一個工具,可惜C語言的表驅(qū)動需要借助函數(shù)指針,它和虛函數(shù)本質(zhì)上一樣,都會導(dǎo)致編譯器放棄很多優(yōu)化,性能都沒有第一種實現(xiàn)高。
那么有沒有一種方法,讓我們可以以表來表示整個FSM,但是運行時效率又能和第一種實現(xiàn)相當(dāng)?前面我們說了,可以利用模板元編程的代碼生成能力。我們利用模板元編程創(chuàng)建一種描述FSM的DSL,讓用戶可以以表的形式描述一個FSM,然后在C++編譯期將其生成類似第一種實現(xiàn)的代碼。這樣我們即得到了吻合于領(lǐng)域的表達力,又沒有造成任何運行時的性能損失!
接下來我們看看如何實現(xiàn)。
既然提到了使用表來表達,那么我們已經(jīng)有了一種熟識的編譯期表數(shù)據(jù)結(jié)構(gòu)了,沒錯,就是TypeList。TypeList的每個元素表示一個轉(zhuǎn)換(transition),代表表的一行。按照前面給出的DanceRobot的表格表示,每行應(yīng)該可以讓用戶定義:當(dāng)前狀態(tài),事件,目標(biāo)狀態(tài),以及對應(yīng)的action。所以我們定義一個模板Row,它的參數(shù)是:int CurrentState, typename EventType, int NextState, void(Fsm::*action)(const EventType&)
,一旦它具現(xiàn)化后將表示一個transition,作為狀態(tài)轉(zhuǎn)換表的一行。
除了表之外,用戶還應(yīng)該負責(zé)給出表中元素的定義,包括每個狀態(tài)、事件和action的定義。我們希望整個DSL框架和用戶的代碼分離開,用戶在自己的類中定義state,event,action以及轉(zhuǎn)換表,然后DSL框架負責(zé)為用戶生成所有的事件處理函數(shù)processEvent
。
有了上面的思考后,我們通過DanceRobot展示我們構(gòu)思的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。最終所有消息處理函數(shù)的生成全部交給DSL背后的fsm::StateMachine
框架,它負責(zé)根據(jù)TransitionTable
生成所有類似前面第一種實現(xiàn)中的processEvent
函數(shù),并且要求性能和它相當(dāng)。fsm::StateMachine
框架是和用戶代碼解耦的,它獨立可復(fù)用的,用戶類通過我們之前介紹過的CRTP技術(shù)和它進行組合。
通過例子可以看到,TransitionTable
的描述已經(jīng)非常接近手工描述一張狀態(tài)表了,我們基本沒有給用戶帶來太多偶發(fā)復(fù)雜度,更重要的是我們完全通過編譯時代碼生成技術(shù)來實現(xiàn),沒有為用戶帶來任何運行時效率損失。
接下來我們具體看看StateMachine
的實現(xiàn)。
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
的代碼實現(xiàn),不要被這么一大坨代碼嚇住,我們一步步分析它的實現(xiàn)。
先來看它的構(gòu)造函數(shù):
StateMachine() : state(Derived::initial)
{
}
int state;
它的內(nèi)部有一個私有成員state,用來存儲當(dāng)前的狀態(tài)。它的構(gòu)造函數(shù)把state初始化為Derived::initial
。得益于CRTP模式,我們在父類模板中可以使用子類中的定義。StateMachine要求其子類中必須定義initial
,用來指明初始狀態(tài)值。
接來下`onUndefined函數(shù)定義了當(dāng)收到未定義的消息時的默認處理方式。可以在子類中重定義這個方法,如果子類中沒有重定義則采用此默認版本。
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;
}
接下來內(nèi)部的嵌套模板Row
用于子類在表中定義一行transition。它的四個模板參數(shù)分別代表當(dāng)前狀態(tài)、事件類型、目標(biāo)狀態(tài)以及對應(yīng)action的函數(shù)指針。注意由于采用了CRTP模式,這里我們直接使用了子類的類型Derived
來聲明函數(shù)指針類型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的調(diào)用進行了封裝,統(tǒng)一了所有action的調(diào)用形式。原有的每個action名稱不同,例如sayReady
、sayStoped
...,后續(xù)可以統(tǒng)一通過調(diào)用Row::execute
的方式進行使用了。借助封裝層來統(tǒng)一不同方法的調(diào)用形式是一種非常有用的設(shè)計技巧。
最后我們來看StateMachine
的processEvent
函數(shù)實現(xiàn)。
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;
}
該函數(shù)是一個模板方法,它的入?yún)⑹谴幚淼南ⅲ瑸榱酥С置糠N消息,將消息的類型定義為泛型。為了方便客戶獲取轉(zhuǎn)換后的目標(biāo)狀態(tài),函數(shù)結(jié)束時返回最新的狀態(tài)。我們期望它對于任一種合法的入?yún)⑾㈩愋停梢宰詣由伤奶幚磉壿嫛?/p>
例如對于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);
上面第一句,我們通過把狀態(tài)表Derived::TransitionTable
和當(dāng)前事件類型交給DispatcherGenerator
,通過它得到了Dispatcher
類型。從第二句中我們知道Dispatcher
類型必須有一個靜態(tài)方法dispatch
,它接收當(dāng)前狀態(tài)和事件對象,然后完成所有的處理邏輯。
所以一切的關(guān)鍵都在于DispatcherGenerator<typename Derived::TransitionTable, Event>::Result
所做的類型生成。它能夠根據(jù)狀態(tài)轉(zhuǎn)化表以及當(dāng)前類型,生成正確的處理邏輯。那么DispatcherGenerator
怎么實現(xiàn)呢?
我們再來看看如下DanceRobot的Close消息處理函數(shù)的實現(xiàn):
if(state == opened)
{
sayClosed(event);
state = closed;
}
else if(state == dancing)
{
sayClosed(event);
state = closed;
}
else
{
reportError(event);
}
我們發(fā)現(xiàn),它的實現(xiàn)是形式化的。就是根據(jù)當(dāng)前消息類型Close
,在狀態(tài)轉(zhuǎn)換表Derived::TransitionTable
中找到所有可以處理它的行:
// +----------+----------+----------+----------------+
// | current | event | target | action |
// +----------+----------+----------+----------------+
Row < opened , Close , closed , &R::sayClosed >,
Row < dancing , Close , closed , &R::sayClosed >
TypeList已經(jīng)有__filter
元函數(shù),它根據(jù)一個指定的規(guī)則,將TypeList中所有滿足條件的元素過濾出來,返回由所有滿足條件的元素組成的TypeList。接下來要做的是用過濾出來的行,遞歸地完成如下模式的if-else
結(jié)構(gòu):
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
中調(diào)用未定義消息的處理函數(shù):
struct DefaultDispatcher
{
template<typename Fsm, typename Event>
static int dispatch(Fsm& fsm, int state, const Event& e)
{
return fsm.onUndefined(state, e);
}
};
到此,基本的思路清楚了,我們把上述生成processEvent
函數(shù)的這一切串起來。
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
是過濾條件,它調(diào)用了EventMatcher
去匹配和DispatcherGenerator
入?yún)⒅?code>Event相同的Transition::Event
。
最后,我們對過濾出來的列表MatchedTransitions
調(diào)用__fold
元函數(shù),將其中每個transition按照EventDispatcher
的模式去遞歸折疊,折疊的默認參數(shù)為DefaultDispatcher
。如此我們按照過濾出來的表行,自動生成了遞歸的if-else
結(jié)構(gòu),該結(jié)構(gòu)存在于返回值類型的靜態(tài)函數(shù)dispatch
中。
這就是整個DSL背后的代碼,該代碼的核心在于利用模板元編程遞歸地生成了每種消息處理函數(shù)中形式化的if-else
嵌套代碼結(jié)構(gòu)。由于模板元編程的所有計算在編譯期,模板中出現(xiàn)的封裝函數(shù)在編譯期都可以被內(nèi)聯(lián),所以最終生成的二進制代碼和最開始我們手寫的是基本一致的。
如果對本例的完整代碼該興趣,可以查看TLP庫中的源碼,位置在"tlp/sample/fsm"中。