C++ 輕量級對象JSON序列化實現詳情
背景:
在項目裡經常遇到對象和json
字符串的相互轉換這類問題,在大多數程序裡,一般這個問題都比較有比較好的解決方法,往往一個函數搞定。但是到瞭c++
這邊卻需要我們手擼json庫一個一個字段初始化/序列化。如果這時候有一個函數 可以一行代碼 unmarshal /marshal
對象豈不是很方便?本文以jsoncpp
庫為基礎,設計這樣一個可以支持這種功能的函數,下面進入正題~
1、設計思路
以unmarshal
為例,我們最終的函數是打造兩個這樣的模板函數 :
一個從string
的josn
直接反序列化對象,一個從jsoncpp
庫的json
對象,反序列化對象。
template<typename T> bool Unmarshal(T& obj,const string& json_str); template<typename T> bool Unmarshal(T& obj,const Json::Value& json_obj_root);
由於json
是具有自遞歸結構的,所以在設計時,應該也是以遞歸的方式解決復雜的組合類,我們可以簡單的把程序中的變量分為下面幾類:
這樣我們隻需要把這幾個場景的 Unmarshal
實現瞭,整體的Unmarshal
也就實現瞭。模板設計的類圖應該和我們的分類相對應:
在實現中要註意以下幾點:
- 每個分類的
Unmarshal
模板具有排他性,也就是說基本類型在編譯期間隻能匹配解析基本類型的模板 - 由於1的保證和這些模板函數名稱都是
Unmarshal
,所以他們之間可以相互嵌套調用對方。 - 指針、和原生數組會涉及到空間分配和長度檢測會使得情況變得復雜,本次設計支持的范圍不包含對指針、原生數組的支持。
2、匹配基本類型的Unmarshal模板
//隻能解析基本類型 int long bool float double string 的一組模板 /* * 其實可聲明為重載函數,聲明為模板隻是為瞭可以放在頭文件中 * 不能聲明為 template <typename T> bool Unmarshal(int& obj,const Json::Value &root);是因為 * 在編譯時編譯器不能推斷出T的類型,導致編譯失敗.所以將模板類型列表設置為template <int = 0> (Nontype * Parameters) 此處int並無實際意義 */ template <int = 0> inline bool Unmarshal(int& obj,const Json::Value &root){ if(!root.isIntegral()) return false; obj = root.asInt(); return true; } template <int = 0> inline bool Unmarshal(long& obj,const Json::Value &root) .....
3、匹配stl容器/其他第三方類庫的Unmarshal模板
//隻能匹配 vector<T> map<string,T> map<long,T> map<int,T> 的一組模板 //vector template <typename T> bool Unmarshal(vector<T>& obj,const Json::Value& root){ if(!root.isArray()) return false; obj.clear(); bool ret = true; for(int i=0;i<root.size();++i){ T tmp; //類型T要含有T()構造函數 if(!Unmarshal(tmp,root[i])) //遞歸調用Unmarshal函數 ret = false; obj.push_back(tmp); } return ret; } //map key:string template <typename T> bool Unmarshal(map<string,T>& obj,const Json::Value& root){ ... } //map key:long template <typename T> bool Unmarshal(map<long,T>& obj,const Json::Value& root){ ... } //map key:int template <typename T> bool Unmarshal(map<int,T>& obj,const Json::Value& root){ ... }
4、匹配自定義struct/class的Unmarshal模板
實現一組隻能匹配自己定義的struct/class
就需要我們定義的對象有一些特殊的標志,才能被模板函數識別。在這裡選擇給我們自己定義的類都實現public的unmarshal
方法(實現方式後面講),這樣當編譯時發現一個對象含有 public
的 unmarshal
方法時,就知道使是我們自己定義的類,然後就調用特定的模板函數,這裡用到到瞭一個C++的語法 SFINAE(Substitution Failure Is Not An Error) 和std庫中enable_if
我們先來看一下C++ std庫中 enable_if 的簡要實現:
// 版本1 一個空的enable_if 結構體 template <bool, class _Tp = void> struct enable_if {}; // 版本2 是版本1第一個參數為true的特例化實現,這個版本的enable_if含有 type類型 template <class _Tp> struct enable_if<true, _Tp> {typedef _Tp type;}; int main(){ enable_if<true,int>::type a = 3; //匹配版本2,相當於 int a = 3 enable_if<false,int>::type b = 3; //匹配版本1,enable_if{}沒有type類型,觸發編譯錯誤 }
SFINAE 準則就是匹配失敗並不是錯誤,如果編譯器在匹配一個模板時引發瞭錯誤,這時候編譯器應當嘗試下一個匹配,而不應該報錯中止。利用這條規則和enable_if,解析我們自己struct/class Umarshal
模板設計如下:
// 檢測一個類 是否含有非靜態非重載的unmarshal方法 template<typename T> struct TestUnmarshalFunc { //版本1 template<typename TT> static char func(decltype(&TT::unmarshal)); //版本2 template<typename TT> static int func(...); /* * 如果類型T沒有unmarshal方法,func<T>(NULL)匹配版本1時會產生錯誤,由於SFINAE準則,隻能匹配版本2 * 的func,此時返回值4個字節,has變量為false.反之 has變量為true */ const static bool has = (sizeof(func<T>(NULL)) == sizeof(char)); }; //如果對象自身含有 unmarshal 方法,則調用對象的unmarshal.否則會因SFINAE準則跳過這個版本的Unamrshal template <typename T,typename enable_if<TestUnmarshalFunc<T>::has,int>::type = 0> inline bool Unmarshal(T& obj,const Json::Value &root){ return obj.unmarshal(root); }
好瞭,至此我們對三種基本類型的Umarshal
函數設計好瞭,這時候任意一個T類型 在調用Unmarshal
時,最終會與上面三種其中一個匹配。json 為string的可以利用上面的Unmarshal
再封裝一個版本:
template <typename T> bool Unmarshal(T& obj,const string &s){ Json::Reader reader; Json::Value root; if(!reader.parse(s,root)) return false; return Unmarshal(obj,root); }
接下來我們看如何在自定義的類中實現unmarshal
函數:
//假設有一個People對象,有3個field需要反序列化,根據上面的要求,可能需要我們自己編寫unmarshal如下 struct People{ bool sex; int age; string name; //盡力解析每個field,隻有全部正確解析才返回true bool unmarshal(const Json::Value &root){ bool ret = true; if(!Json::Unmarshal(sex,root["sex"])){ ret = false; } if(!Json::Unmarshal(age,root["age"])){ ret = false; } if(!Json::Unmarshal(name,root["name"])){ ret = false; } return ret; } };
顯然如果field數量很多,就很麻煩,而且解析每個field
時,代碼格式非常相似,是否存在一個宏可以自動生成呢?答案是肯定的。talk is cheap
,show me the code
,上代碼!
struct People{ bool sex; int age; string name; //用JSON_HELP宏把需要序列化的field傳進去,就自動在類裡生成unmarshal、marshal函數 JSON_HELP(sex,age,name) }; // JSON_HELP 是一個變參宏 #define JSON_HELP(...) \ UNMARSHAL_OBJ(__VA_ARGS__) \ //這裡生成unmarshal函數 MARSHAL_OBJ(__VA_ARGS__) /* * UNMARSHAL_OBJ中FOR_EACH宏第一個參數傳入一個函數,第二個參數傳入一個list * 作用是對list中每個元素調用傳入的函數,這裡有點像python裡高階函數map()的味道 * 這樣就批量生成瞭下面的代碼結構: * if(!Json::Unmarshal(field_1,root["field_1"])){ * ret = false; * } * if(!Json::Unmarshal(field_2,root["field_2"])){ * ret = false; * } * ... .. */ #define UNMARSHAL_OBJ(...) \ bool unmarshal(const Json::Value& root){ \ bool ret = true; \ FOR_EACH(__unmarshal_obj_each_field__,__VA_ARGS__) \ return ret; \ } #define __unmarshal_obj_each_field__(field) \ if(!Json::Unmarshal(field,root[#field])){ \ ret = false; \ } //###### FOR_EACH 實現####### //作用:傳入一個函數func和一個list,把func用在list的每個元素上 #define FOR_EACH(func,...) \ MACRO_CAT(__func_,COUNT(__VA_ARGS__))(func,__VA_ARGS__) /* * FOR_EACH在實現中 COUNT宏用於統計參數個數,返回一個數字,MACRO_CAT宏用於把兩個token連接起來, * 如果__VA_ARGS__有三個變量為a,b,c 那麼這一步宏展開後為: * __func_3(func,a,b,c), 而__func_3 __func_2 __func_1 ... 定義如下 * / // 宏展開實現偽循環 /* * __func_3(func,a,b,c) 具體展開過程: * 第一次: __func_1(func,a) __func_2(func,b,c) * 第二次: func(a) __func_1(func,b) __func_1(func,c) * 第三次: func(a) func(b) func(c) * 最終在a,b,c上都調用瞭一次傳入的func函數 */ #define __func_1(func,member) func(member); #define __func_2(func,member,...) __func_1(func,member) __func_1(func,__VA_ARGS__) #define __func_3(func,member,...) __func_1(func,member) __func_2(func,__VA_ARGS__) #define __func_4(func,member,...) __func_1(func,member) __func_3(func,__VA_ARGS__) #define __func_5(func,member,...) __func_1(func,member) __func_4(func,__VA_ARGS__) ... ... //###### COUNT 宏實現####### //作用: 返回傳入參數個數. eg: COUNT(a,b,c)返回3 #define COUNT(...) __count__(0, ##__VA_ARGS__, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0) #define __count__(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, N, ...) N ###### MACRO_CAT 宏實現####### //作用: 將兩個token(可以是宏),連接在一起 #define MACRO_CAT(a,b) __macro_cat__(a,b) #define __macro_cat__(a,b) a##b
5、測試
我們的Umarshal
和 Marshal
函數編寫好瞭,現在測試一下吧:
場景:有一個map
對象存著教師的信息,每個教師又保存著ta教學生信息,數據結構定義如下:
struct Student { long id; bool sex; double score; string name; JSON_HELP(id,sex,score,name) }; struct Teacher { string name; int subject; vector<Student> stus; JSON_HELP(name,subject,stus) }; map<string,Teacher> tchs; //需要序列化和反序列化的對象
測試代碼:
// 對應於結構 map<string,Teacher> 的json string ori = R"( { "Tea_1": { "name": "Tea_1", "subject": 3, "stus": [ { "id": 201721020126, "sex": false, "score": 80, "name": "Stu.a" }, { "id": 201101101537, "sex": true, "score": 0, "name": "Stu.b" } ] }, "Tea_2": { "name": "Tea_2", "subject": 1, "stus": [ { "id": 201521020128, "sex": true, "score": 59, "name": "Stu.c" } ] } } )"; int main() { map<string,Teacher> tchs; // 從json字符串反序列化對象 bool ret = Json::Unmarshal(tchs,ori); if(!ret){ cout<<"反序列失敗"<<endl; return 0; }else{ cout<<"反序列成功"<<endl; } // 序列化對象到 json字符串 cout<<"輸出對象序列化的json:"<<endl; string obj2json; Json::Marshal(tchs,obj2json); cout<<obj2json; } //##### 輸出結果##### 反序列成功 輸出對象序列化的json: {"Tea_1":{"name":"Tea_1","stus":[{"id":201721020126,"name":"Stu.a","score":80.0,"sex":false},{
到此這篇關於C++ 輕量級對象JSON序列化實現詳情的文章就介紹到這瞭,更多相關C++ 輕量級對象JSON序列化實現內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- c++元編程模板函數重載匹配規則示例詳解
- go語言中json數據的讀取和寫出操作
- 淺談C++11的std::function源碼解析
- 深入淺析C++ traits技術
- Golang中結構體映射mapstructure庫深入詳解