C++中模擬反射填充消息struct

問題

我正作的一個項目須要在Erlang 節點和C++ 節點之間傳輸大量的事件,在C++這一側,使用struct儲存這些消息。node

天然的,我須要不少struct,例如:c++

struct msg_greeting{
    std::string sender;
    std::string content;
    int         content_length;
    std::string content_type;
};
struct msg_bye{
    std::string sender;
};

在Erlang這一側,使用tuple儲存,因爲Erlang是動態類型的,因此不須要定義,這裏只是說明:shell

{greeting, Sender ::string(), Content ::string(), ContentLength ::int(), ContentType ::atom() }
{bye, Sender ::string() }

消息的傳輸可使用tinch_pp (http://www.adampetersen.se/code/tinchpp.htm) 編程

若是你第一次使用tinch_pp,下面這一段是一個簡單的接收和匹配的過程,即便不瞭解tinch_pp也能夠看懂:
微信

void connect::msg_receiver_routine()
{
    try{
        while(1) {
            matchable_ptr msg = mbox->receive();
            int token;

            std::string type;
            matchable_ptr body;
            if(msg->match(
                make_e_tuple(atom("event"),
                e_string(&type)),
                any(&body)))
                //do something here
            else
                //some log here
        }
    }catch(boost::thread_interrupted e){
    // @todo output some log here
    }
};

咱們使用event標識一個erlang事件,type是這個事件的類型,body是事件內容,也就是咱們以前定義的greeting或者bye。cookie

接下來,咱們須要實現事件的處理,首先,咱們須要把tinch_pp匹配出來的tuple填入咱們的c++結構。函數

咱們這樣作:測試

msg_ptr on_greeting(matchable_ptr p){
    std::string sender;
    std::string content;
    int contentLength;
    std::string contentType;
    bool matched = p->match(make_e_tuple(
        erl::string(&sender),
        erl::string(&content),
        erl::int_(&contentLength),
        erl::atom(&contentType)
    ));
    
    if(matched){
        msg_ptr = shared_ptr<msg_greeting>(new msg_greeting());
        msg_ptr->Sender = sender;
        msg_ptr->Content = content;
        msg_ptr->ContentLength = contentLength;
        msg_ptr->ContentType = contentType;
        return msg_ptr;
    }

    return shared_ptr<msg_greeting>();
}

問題在於,咱們須要爲每一個消息寫這麼一大段代碼。假如咱們的C Node須要處理幾十種消息,咱們就須要把這個代碼重複幾十遍,而實際上只有一小部分纔是有用的(有差別的)。atom

提取通用代碼

怎樣才能省去重複的部分,只保留其中的精華呢?這就須要元編程和預處理器了,咱們稍後再介紹。spa

首先,最顯著的差別就是不一樣的消息中的信息不同,用c++的說法是:他們具備不一樣的成員。

去掉這個差別後,咱們的代碼能夠簡化爲:

msg_ptr on_greeting(matchable_ptr p){    
    if(matched){
        msg_ptr mp = msg_greeting::make(p);
        return mp;
    }

    return shared_ptr<msg_greeting>();
}

看似簡潔了許多,但實際上,咱們只是把msg_greeting特有的處理(差別)隱藏在msg_greeting定義的靜態方法裏了。

至少,咱們的on_xxxx方法看起來乾淨點了。

可是,咱們仍是須要在某處(msg_greeting內部)定義這些代碼。

更好的方案

反射是不少語言都具備的特性,反射意味着類具備了自省的能力,即一個類知道本身有哪些成員,這些成員有哪些屬性。

若是C++支持反射,咱們這個問題就好解決了,咱們能夠定義一個msg_fill方法,按照msg成員的屬性,從matchable_ptr獲取成員的值。等等,C++可沒有反射支持,至少我不知道。

那麼,咱們本身來實現一個吧。

成員屬性

咱們須要一個能保存成員屬性的,符合C++語法的物件。有兩種選擇:對象,類型。

對象對應着運行時,類型對應着編譯時。考慮到速度和效率,咱們選擇類型。

在C++進行元編程,主要是依靠模板來實現的,首先咱們聲明一個模板,用來表示成員

template <class Type ,class Struct, Type(Struct::*Field)>
struct auto_field;

這個模板有三個參數:Type表示成員的C++類型,Struct表示這個成員所屬的結構,Field是成員指針,用來記住這個成員在所屬結構中所處的位置。

光有聲明沒有什麼做用,因此咱們須要一些實現(或者說模板定義):

 template <class Struct, bool(Struct::*Field)>
 struct auto_field<bool, Struct, Field>{
     typedef tinch_pp::erl::atom field_e_type;
     typedef std::string field_c_type;
     
     static void fill(Struct* s, field_c_type& c){
         s->*Field = (c == "true");
     }; 
 };

能夠看出,咱們經過模板特化,爲bool類型的成員提供了:

  • C++類型

  • Erlang類型

  • 填充C++類型的fill方法

這裏其實隱藏了一個問題,怎麼知道須要定義這幾個類型和靜態成員函數呢?稍後再介紹。

相似的,咱們能夠爲更多的類型提供特化,再也不重複。

至此,咱們已經知道怎麼定義類型成員,並記住成員的屬性。

填充數據

有了成員的屬性,咱們就能夠解析消息tuple了,參考最初的代碼,填充方法的僞實現應該長這樣:

template <class Msg>
bool fill(Msg* e){
    field_0_c_type field_0_c;
    field_1_c_type field_1_c;    
    field_2_c_type field_2_c;    

    bool matched = p->match(make_e_tuple(
        field_0_e_type(&field_0_c),
        field_1_e_type(&field_1_c),
        field_2_e_type(&field_2_c)
    ));
    
    if(matched){
        Event::fill(e,field_0_c);
        Event::fill(e,field_1_c);
        Event::fill(e,field_2_c);
        return true;
    }

    return false;
};

到此,咱們發現不一樣事件的成員數目是不一樣的,因此,上述僞代碼只能適應成員數爲3的消息。

那麼,咱們就須要提供一組fill實現,每一個負責一個成員數。一樣,使用模板參數和模板特化來實現:

template <int Size,class Msg>
bool fill(Msg* e);

template <class Msg>
bool fill<1,Msg>(Msg* e){
    field_0_c_type field_0_c;

    bool matched = p->match(make_e_tuple(
        field_0_e_type(&field_0_c)
    ));
    
    if(matched){
        Event::fill(e,field_0_c);
        return true;
    }
    return false;
};

template <class Msg>
bool fill<2,Msg>(Msg* e)
    field_0_c_type field_0_c;
    field_1_c_type field_1_c;
    ......

額~ 這不是又重複了嗎?

別急,咱們能夠用boost::preprocess收斂這些實現,boost::preprocess用來生成重複的代碼,

使用後,咱們的fill方法長這樣:

namespace aux {
template <int FieldListSize,typename FieldList>
struct fill_impl;
#define EMATCH_MAX_FIELD 8 

#define BOOST_PP_ITERATION_LIMITS (1,8)
#define BOOST_PP_FILENAME_1 <e_match_impl.h> 
#include BOOST_PP_ITERATE()
};
template<typename FieldList>
struct fill : aux::fill_impl<boost::mpl::size<FieldList>::type::value , FieldList>{
};

怎麼回事?fill方法消失了?

不,並無消失,咱們把他隱藏在

e_match_impl.h

這個文件裏,經過boost::preprocess重複include這個文件8次,從而得到1個到8個成員的fill實現。並經過集成把這個實現模板提供的功能暴露出來,同時收斂其模板參數。

至此,咱們獲得了一個能夠根據FieldList(成員屬性的mpl::list),自動match和填充C++結構的fill方法。


使用

好了,咱們來寫一段代碼,測試一下上述實現吧:

struct SimpleObject{	
	bool			b;
	std::string		s;		
};

typedef boost::mpl::list<
	auto_field<bool,		SimpleObject,	&SimpleObject::b>,
	auto_field<std::string,	SimpleObject,	&SimpleObject::s>
> SimpleObjectFields;


int _tmain(int argc, _TCHAR* argv[])
{
	SimpleObject so;

	const std::string remote_node_name("testnode@127.0.0.1");
	const std::string to_name("reflect_msg");

	tinch_pp::node_ptr my_node = tinch_pp::node::create("my_test_node@127.0.0.1", "abcdef");

	tinch_pp::mailbox_ptr mbox = my_node->create_mailbox();

	mbox->send(to_name, remote_node_name, tinch_pp::erl::make_e_tuple(tinch_pp::erl::atom("echo"), tinch_pp::erl::pid(mbox->self()), tinch_pp::erl::make_e_tuple(
		tinch_pp::erl::make_atom("false"),
		tinch_pp::erl::make_string("hello c++")
		)));
	
	const tinch_pp::matchable_ptr reply = mbox->receive();

	bool ret = fill<SimpleObjectFields>::fill_on_match(&so,reply);
	printf("ret is %s \n",(ret?"true":"false"));
	printf("so.b == %s \n",(so.b?"true":"false"));
	printf("so.s == %s \n",so.s.c_str());
	system("pause");
	return 0;
}

因爲我沒有找到tinch_pp怎麼構造一個matchable_ptr,因此須要一個erlang的外部節點把我構造的tuple反射回來,tinch_pp已經提供了這樣的一個server,運行上述代碼前,須要先把他啓動起來:

werl -pa . -sname testnode -setcookie abcdef

運行後,應該打印出:

ret is true
so.b == false
so.s == hello c++
請按任意鍵繼續. . .

至此,咱們實現了想要的功能,使用同一份代碼(fill)將Erlang tuple直接填充到指定的C++結構中,而沒必要大量重複填充代碼。

歡迎關注個人微信帳號,不按期推送博客更新。

相關文章
相關標籤/搜索