C++開源庫nlohmann/json的介紹和使用詳解
前言
該庫的地址:github.com/nlohmann/json,到目前已經(jīng)擁有38.5K的star,可以說是被大家廣泛贊同,經(jīng)過簡單探究,確實(shí)發(fā)現(xiàn)非常不錯(cuò),可以在項(xiàng)目中使用。
該庫的集成非常容易,只有1個(gè)hpp
文件,拿來即用,完全不需要任何復(fù)雜的編譯,尤其是剛?cè)腴T的C++開發(fā)來說,編譯一些開源庫是永遠(yuǎn)的噩夢。
同時(shí)該庫使用C++11代碼編譯,使用了不少特性,也是值得學(xué)習(xí)其原理,學(xué)習(xí)C++的不錯(cuò)選擇。
下面安裝其readme
,簡單介紹,以及自己的使用體驗(yàn)。
1. JSON as first-class data type
中文翻譯為json作為一等數(shù)據(jù)類型,即我們可以將json數(shù)據(jù)類型作為一等公民(first-class citizen
),并且支持相應(yīng)的操作和語法。
關(guān)于某種語言是否把某個(gè)特性或者功能作為一等公民看待,我個(gè)人有2點(diǎn)感受比較深刻。第一個(gè)是Java到Kotlin的編程語言變化,同為JVM語言,Kotlin把函數(shù)和lambda表達(dá)式看成一等公民,在Kotlin中可以將函數(shù)作為參數(shù)、作為返回值等,在Kotlin官方實(shí)例和源碼中大量使用,極大地提高代碼簡潔性。第二個(gè)是學(xué)習(xí)C++時(shí),C++沒有把原生數(shù)組類型看成一等公民,比如數(shù)組不支持直接拷貝賦值,原因很多,比如必須固定長度,缺乏高級操作以及語法糖支持,指針語義容易混淆(C++中,數(shù)組名實(shí)際上是一個(gè)指向數(shù)組首元素的指針)等,所以推薦使用std::vector
來替代原生數(shù)組。
在C++中并沒有直接將json看成一等公民,與此對應(yīng)的Python就有非常好的支持,而該庫想實(shí)現(xiàn)這一目的,就必須得提供足夠簡潔的使用方式,以及足夠豐富的操作。比如我想創(chuàng)建一個(gè)json對象,如下:
//想創(chuàng)建的json對象,標(biāo)準(zhǔn)json格式 { "pi": 3.141, "happy": true, "name": "Niels", "nothing": null, "answer": { "everything": 42 }, "list": [1, 0, 2], "object": { "currency": "USD", "value": 42.99 } } ? //代碼如下 //創(chuàng)建一個(gè)空對象 json j; //增加一個(gè)數(shù)字,使用double類型存儲, j["pi"] = 3.141; //增加一個(gè)布爾值,使用bool類型存儲 j["happy"] = true; //增加一個(gè)字符串,使用std::string類型存儲 j["name"] = "Niels"; //通過傳遞一個(gè)nullptr增加一個(gè)空對象 j["nothing"] = nullptr; //在對象內(nèi)部增加一個(gè)對象 j["answer"]["everything"] = 42; //增加一個(gè)數(shù)組,使用std::vector j["list"] = {1, 0, 2}; //增加另一個(gè)對象 j["object"] = { {"currency", "USD"}, {"value", 42.99}}; //或者使用更方便的方式 json j2 = { {"pi", 3.141}, {"happy", true}, {"name", "Niels"}, {"nothing", nullptr}, {"answer", { {"everything", 42} }}, {"list", {1, 0, 2}}, {"object", { {"currency", "USD"}, {"value", 42.99} }} }; std::cout << j2.dump(4) << std::endl; //運(yùn)行結(jié)果 { "answer": { "everything": 42 }, "happy": true, "list": [ 1, 0, 2 ], "name": "Niels", "nothing": null, "object": { "currency": "USD", "value": 42.99 }, "pi": 3.141 }
這里看似非常簡單,但是有一些原理我們是需要知道的。我們對比一下給定的json
中list
的結(jié)構(gòu),它是一個(gè)數(shù)組,使用[1, 0, 2]
,我們在j
和j2
的構(gòu)造中,都是使用的{}
來進(jìn)行賦值的,這一點(diǎn)就和把json看成一等公民的Python語言有較大區(qū)別,下面是Python代碼:
import sys import os import json ? # 定義一個(gè) Python 對象 person = { "name": "Alice", "age": 25, "hobby": ["reading", "music"] } ? # 將 Python 對象轉(zhuǎn)換為 json 字符串 json_str = json.dumps(person) ? # 輸出轉(zhuǎn)換后的 JSON 字符串 print(json_str) ? //輸出結(jié)果 {"name": "Alice", "age": 25, "hobby": ["reading", "music"]}
可以發(fā)現(xiàn)在Python構(gòu)建對象時(shí),對于數(shù)組結(jié)構(gòu),可以直接使用[]
,更符合常識,C++為什么不可以,因?yàn)?code>j["list"] = {1, 0, 2};是通過重載[]
運(yùn)算符對j
進(jìn)行賦值,默認(rèn)的數(shù)據(jù)成員對象類型是std::vector
,對std::vector
的初始化使用列表初始化時(shí),不能使用[]
,即如下:
//C++11可以使用列表初始化 std::vector list = {0, 1, 2}; //錯(cuò)誤 std::vector list1 = [0, 1, 2];
因?yàn)樵搸熘荒芾弥剌d運(yùn)算符等方式來讓json操作看起來像是操作json,而非C++語言支持這種操作。
搞清楚這個(gè)之后,我們再來看看object
,在原始的json
中,我們可以清晰地知道它對應(yīng)的類型是一個(gè)類類型:
"object": { "currency": "USD", "value": 42.99 }
但是在使用對j
的object
的賦值如下:
j["object"] = { {"currency", "USD"}, {"value", 42.99}};
這不是和前面所說的數(shù)組賦值沖突了嗎,依據(jù)列表初始化規(guī)則,這個(gè)完全可以解析為數(shù)組類型,即[["currency", "USD"],["value", 42.99]]
,也就是二維數(shù)組,這時(shí)我們又可以對比一下Python可以怎么寫:
import sys import os import json ? # 定義一個(gè) Python 對象 person = { "name": "Alice", "age": 25, "object": { "currency":"USD", "value":"42.99" } } ? # 將 Python 對象轉(zhuǎn)換為 JSON 字符串 json_str = json.dumps(person) ? # 輸出轉(zhuǎn)換后的 JSON 字符串 print(json_str) ? #輸出結(jié)果 {"name": "Alice", "age": 25, "object": {"currency": "USD", "value": "42.99"}}
可以發(fā)現(xiàn)在Python中可以使用:
,這樣對應(yīng)的鍵和值非常容易識別,那C++為什么不可以呢?還是一樣的問題,在代碼j["object"] = { {"currency", "USD"}, {"value", 42.99}};
中,是使用std::map
數(shù)據(jù)結(jié)構(gòu)來作為其默認(rèn)的成員數(shù)據(jù)類型,而C++11中可以使用列表初始化來初始化std::map
,同時(shí)沒有:
的語法:
//C++11可以使用列表初始化 std::map map = { {"currency", "USD"}, {"value", 42.99} }; //錯(cuò)誤 std::map map1 = { {"currency":"USD"}, {"value":42.99} };
搞明白為什么之后,我們就要回答前面的問題,為什么object
沒有被解析為std::vector
類型呢?原因是默認(rèn)規(guī)則就是這樣的,即一層和2層{}
的默認(rèn)處理邏輯是不一樣的。
假如有一些極限情況,我就是想用數(shù)組形式來保存對象格式,這時(shí)可以顯示地聲明json值的類型,使用json::array()
和json::object()
函數(shù):
//C++11可以使用列表初始化 std::map map = { {"currency", "USD"}, {"value", 42.99} }; //錯(cuò)誤 std::map map1 = { {"currency":"USD"}, {"value":42.99} };
搞明白為什么之后,我們就要回答前面的問題,為什么object
沒有被解析為std::vector
類型呢?原因是默認(rèn)規(guī)則就是這樣的,即一層和2層{}
的默認(rèn)處理邏輯是不一樣的。
假如有一些極限情況,我就是想用數(shù)組形式來保存對象格式,這時(shí)可以顯示地聲明json值的類型,使用json::array()
和json::object()
函數(shù):
//顯示聲明是一個(gè)數(shù)組,而非對象 json empty_array_explicit = json::array(); //對于沒有構(gòu)造函數(shù)或者這種,默認(rèn)隱式類型是對象 json empty_object_implicit = json({}); json empty_object_explicit = json::object(); //是一個(gè)數(shù)組,而非對象 json array_not_object = json::array({ {"currency", "USD"}, {"value", 42.99} }); std::cout << empty_array_explicit.dump(4) << std::endl; std::cout << empty_object_implicit.dump(4) << std::endl; std::cout << empty_object_explicit.dump(4) << std::endl; std::cout << array_not_object.dump(4) << std::endl; //運(yùn)行結(jié)果 [] {} {} [ [ "currency", "USD" ], [ "value", 42.99 ] ]
搞清楚默認(rèn)類型,以及如何顯示聲明非常重要。
2. 序列化/反序列化
既然想把json打造為一等公民,序列化和反序列化是必須要具備 ,而且要可以從不同的源來進(jìn)行序列化/反序列化,比如從文件、字符串等。
2.1 與字符串
我們可以從字符串字面量來創(chuàng)建一個(gè)json
對象,注意和上面使用=
進(jìn)行列表初始化的方式不同,這里的參數(shù)是字符串字面量:
json j = "{"happy":true,"pi":3.141}"_json; std::cout << j.dump(4) << std::endl; auto j2 = R"({ "happy": true, "pi": 3.141 })"_json; std::cout << j2.dump(4) << std::endl; json j1 = "{"happy":true,"pi":3.141}"; std::cout << j1.dump(4) << std::endl; //運(yùn)行結(jié)果 { "happy": true, "pi": 3.141 } { "happy": true, "pi": 3.141 } "{"happy":true,"pi":3.141}"
上面代碼都是通過字符串初始化一個(gè)json
對象,但是需要注意的點(diǎn)很多。
2.1.1 原始字符串字面量(Raw String Literal)
首先就是j2
的初始化,它使用了R"()"
這種語法,這種表示字符串的方式叫做原始字符串字面量。在C++中,可以使用R"()"
來表示原始字符串字面量,使用原始字符串字面量可以方便地包含特殊字符(比如反斜杠、引號等)或者多行文本的字符串,而無需對這些特殊字符進(jìn)行轉(zhuǎn)移。
比如如下代碼:
{ qDebug() << "Hello\tWorld!"; qDebug() << R"(Hello\tWorld NEW LINE)"; }
運(yùn)行結(jié)果是:
可以發(fā)現(xiàn)普通字符串中\t
被解釋為制表符,而在原始字符串字面量中,\t
不會被解析,并且換行也被保留了下來。其實(shí)在其他語言中,使用這種方式更為簡單,比如Kotlin使用"""
進(jìn)行包裹字符串,或者Python使用```進(jìn)行包裹字符串。
通過這個(gè)小知識點(diǎn)的學(xué)習(xí),我們明顯發(fā)現(xiàn)j2
就比j
的初始化方式更人性化,至少不用去在意字符串中的轉(zhuǎn)移字符。
2.1.2 自定義字面量操作符
然后我們關(guān)注點(diǎn)來到j
和j1
的區(qū)別,可以發(fā)現(xiàn)j
多了一個(gè)_json
的后綴,然后在輸出打印中就可以正常解析,而j1
卻不可以。這里的核心點(diǎn)就是字符串字面量后面的_json
,我們看一下它的源碼:
JSON_HEDLEY_NON_NULL(1) inline nlohmann::json operator "" _json(const char* s, std::size_t n) { return nlohmann::json::parse(s, s + n); }
看到operator
就應(yīng)該想到自定義操作符,沒錯(cuò),operator ""
是C++11引入的自定義字面量操作符,通過重載operator ""
可以為特定的后綴自定義語義,這種機(jī)制可以使得我們可以像使用內(nèi)置的字面量(如整數(shù)、浮點(diǎn)數(shù)、字符串等)一樣自然地使用自定義的字面量,從而提高代碼的可讀性和表達(dá)力。
舉個(gè)簡單的例子:
//為long long類型定義一個(gè)_km后綴 constexpr long long operator "" _km(unsigned long long x) { return x * 1000; } long long distance = 20_km; std::cout << "distance:" << distance << "meters\n"; //輸出結(jié)果 distance:20000meters
從這個(gè)例子我們來看上面_json
的含義,它就是給const char*
類型即字符串類型添加一個(gè)_json
后綴,作用就是調(diào)用nlohmann::json::parse(s, s + n)
返回一個(gè)json
對象,理解了這個(gè)之后,我們也就可以理解為什么j1
不能被解析,因?yàn)闆]有_json
后綴,根本不會調(diào)用parse
函數(shù)進(jìn)行解析成json
對象。
因?yàn)?code>_json這種用法就是語法糖,所以其實(shí)想解析一個(gè)字符串成json
,就可以直接調(diào)用json::parse()
函數(shù):
json j3 = json::parse(R"({"happy": true,"pi": 3.141})");
從前面的錯(cuò)誤用法打印來看,一般情況下我們認(rèn)為json
對象要不表示一個(gè)對象,要不表示一個(gè)數(shù)組,但是從打印j1
來看:
json j1 = "{"happy":true,"pi":3.141}"; std::cout << j1.dump(4) << std::endl; //運(yùn)行結(jié)果 "{"happy":true,"pi":3.141}"
這里居然返回一個(gè)字符串。所以這里一定要區(qū)分序列化和賦值的區(qū)別,對于序列化,在前面我們說了2種方式,一種是使用列表初始化,一種是使用字符串字面量,而直接賦值的話,json
內(nèi)部會直接保存一個(gè)字符串。
2.2 與file
從文件中反序列化一個(gè)json
對象可以說是在配置文件時(shí)非常常用了,這里使用也非常簡單,代碼如下:
std::string dirPath = QApplication::applicationDirPath().toStdString(); std::string file = dirPath + "/file.json"; //使用ifstream讀取file std::ifstream i(file); json j_file; //把輸入流中的信息放入json中 i >> j_file; std::cout << j_file.dump(4) << std::endl; json j_file_bak = { {"pi", 3.141}, {"happy", true}, {"name", "Niels"}, {"nothing", nullptr}, { "answer", { {"everything", 42} } }, {"list", {1, 0, 2}}, { "object", { {"currency", "USD"}, {"value", 42.99} } } }; std::string file_bak = dirPath + "/file_bak.json"; std::ofstream o(file_bak); o << std::setw(4) << j_file_bak << std::endl;
可以發(fā)現(xiàn)只要獲取到標(biāo)準(zhǔn)的輸入輸出流之后,我們就可以使用>>
和<<
符號來進(jìn)行文件序列化和反序列化了,這里的原理非常簡單,也是操作符重載,在源碼中重載了>>
和<<
:
friend std::istream& operator>>(std::istream& i, basic_json& j) { parser(detail::input_adapter(i)).parse(false, j); return i; } friend std::ostream& operator<<(std::ostream& o, const basic_json& j) { // read width member and use it as indentation parameter if nonzero const bool pretty_print = o.width() > 0; const auto indentation = pretty_print ? o.width() : 0; // reset width to 0 for subsequent calls to this stream o.width(0); // do the actual serialization serializer s(detail::output_adapter<char>(o), o.fill()); s.dump(j, pretty_print, false, static_cast<unsigned int>(indentation)); return o; }
對于C++開發(fā)來說,左移和右移符號已經(jīng)非常熟悉了。
3. STL-like access
STL-like access
是STL風(fēng)格訪問的意思,在C++中可以為一些類定義STL風(fēng)格訪問的API,可以提高類型的靈活性和易用性,這樣我們就可以像STL容器一樣使用迭代器、算法等功能,從而可以簡化很多操作,提高了代碼的可讀性和可維護(hù)性。
3.1 STL風(fēng)格API
由于json
本身就是由標(biāo)準(zhǔn)庫中的類型進(jìn)行解析的,所以為其設(shè)計(jì)一套STL風(fēng)格訪問的API也就很有必要,使用如下:
//創(chuàng)建一個(gè)數(shù)組, 使用push_back json j; j.push_back("foo"); j.push_back(1); j.push_back(true); //可以使用emplace_back j.emplace_back(1.78); //使用迭代器遍歷數(shù)組 for (json::iterator it = j.begin(); it != j.end(); it++) { std::cout << *it << std::endl; } //快速for循環(huán) for (auto& element : j) { std::cout << element << std::endl; } //getter/setter const auto tmp = j[0].template get<std::string>(); j[1] = 42; bool foo = j.at(2); //比較運(yùn)算符 j == R"(["foo", 1, true, 1.78])"_json; j.size(); j.empty(); j.type(); j.clear(); //快捷類型判斷 j.is_null(); j.is_boolean(); j.is_number(); j.is_object(); j.is_array(); j.is_string(); //創(chuàng)建一個(gè)對象 json o; o["foo"] = 23; o["bar"] = false; o["baz"] = 3.14; //特殊的成員迭代器函數(shù) for (json::iterator it = o.begin(); it != o.end(); it++) { std::cout << it.key() << " : " << it.value() << std::endl; } //快速for循環(huán) for (auto& el : o.items()) { std::cout << el.key() << " : " << el.value() << std::endl; } //c++17特性,結(jié)構(gòu)化綁定 for (auto& [key, value] : o.items()) { std::cout << key << " : " << value << std::endl; } //判斷有沒有某個(gè)鍵值對 if (o.contains("foo")) { std::cout << "contain foo" << std::endl; }
這種使用方式可以讓我們操作json像操作標(biāo)準(zhǔn)容器一樣,非常方便。
3.2 從STL容器構(gòu)造
同理,我們可以從STL容器構(gòu)造出json對象,任何序列容器,比如std::array
、std::vector
、std::deque
、std::forward_list
和std::list
,其中保存值可以構(gòu)造json的值,比如int
,float
、boolean
、字符串類型等,這些容器均可以用來構(gòu)造json數(shù)組。對于類似的關(guān)聯(lián)容器(std::set
、std::multiset
、std::unordered_set
、std::unordered_multiset
)也是一樣,但是在這種情況下,數(shù)組元素的順序取決于元素在數(shù)組中的排序方式。
使用代碼如下:
std::vector<int> c_vector {1, 2, 3, 4}; json j_vec(c_vector); std::deque<double> c_deque {1,2, 2.3, 3.4, 5.6}; json j_deque(c_deque); std::list<bool> c_list {true, true, false, true}; json j_list(c_list); std::forward_list<int64_t> c_flist {12345678909876, 23456789098765, 34567890987654, 45678909876543}; json j_flist(c_flist); std::array<unsigned long, 4> c_array{{1, 2, 3, 4}}; json j_array(c_array); std::set<std::string> c_set {"one", "two", "three", "four", "one"}; json j_set(c_set); std::unordered_set<std::string> c_uset {"one", "two", "three", "four", "one"}; json j_uset(c_uset); std::multiset<std::string> c_mset {"one", "two", "one", "four"}; json j_mset(c_mset); std::unordered_multiset<std::string> c_umset {"one", "two", "one", "four"}; json j_umset(c_umset);
關(guān)于這幾種STL容器,可以做個(gè)簡單的概述:
容器類型 | 底層實(shí)現(xiàn) | 特點(diǎn) | 適用場景 |
---|---|---|---|
std::vector | 動態(tài)數(shù)組 | 可變大小的連續(xù)存儲空間,支持隨機(jī)訪問,尾部插入和刪除效率高。 | 需要在末尾進(jìn)行頻繁插入和刪除操作,以及隨機(jī)訪問元素的情況。 |
std::array | 靜態(tài)數(shù)組 | 固定大小的連續(xù)的靜態(tài)數(shù)組,在編譯期就確定了大小,并且不會改變。 | 替代原生數(shù)組,有更多的成員函數(shù)與操作API。 |
std::list | 雙向鏈表 | 支持雙向迭代器,插入和刪除元素效率高,不支持隨機(jī)訪問。 | 需要頻繁在中間位置進(jìn)行插入和刪除操作,不需要隨機(jī)訪問元素。 |
std::deque | 雙端隊(duì)列 | 支持隨機(jī)訪問,支持在兩端插入和刪除元素,動態(tài)地分配存儲空間。 | 需要在兩端插入和刪除元素,并且需要隨機(jī)訪問元素的情況。 |
std::set | 紅黑樹 | 自動排序元素,不允許重復(fù)元素,插入和刪除的時(shí)間復(fù)雜度為O(logN)。 | 需要自動排序且不允許重復(fù)元素的情況。 |
std::multiset | 紅黑樹 | 自動排序元素,允許重復(fù)元素,插入和刪除的時(shí)間復(fù)雜度為O(1)。 | 需要自動排序且允許重復(fù)元素的情況。 |
std::unordered_set | 哈希表 | 無序存儲元素,不允許重復(fù)元素,插入和查找的時(shí)間復(fù)雜度為O(1)。 | 不需要排序,并且不允許重復(fù)元素的情況。 |
std::unordered_multiset | 哈希表 | 無序存儲元素,允許重復(fù)元素,插入和查找的時(shí)間復(fù)雜度為O(1)。 | 不需要排序,并且允許重復(fù)元素的情況。 |
類似的,STL的鍵值對容器,只要鍵可以構(gòu)造std::string
對象,值可以構(gòu)造json對象的,也可以用來構(gòu)造json對象類型,測試代碼如下:
std::map<std::string, int> c_map { {"one", 1}, {"two", 2}, {"three", 3} }; json j_map(c_map); std::unordered_map<const char*, double> c_umap { {"one", 1.2}, {"two", 2.3}, {"three", 3.4} }; json j_umap(c_umap); std::multimap<std::string, bool> c_mmap { {"one", true}, {"two", false}, {"three", false}, {"three", true} }; json j_mmap(c_mmap); std::unordered_multimap<std::string, bool> c_ummap { {"one", true}, {"two", true}, {"three", false}, {"three", true} }; json j_ummap(c_ummap);
這幾種容器也做個(gè)概述,和std::set
類似,也是從是否自動排序和重復(fù)(一鍵多值)這兩個(gè)維度來擴(kuò)展,和Java還是有一點(diǎn)區(qū)別的,在Java中使用最多的是HashMap
,類似C++
的std::unordered_set
:
容器類型 | 底層 | 特點(diǎn) |
---|---|---|
std::map | 紅黑樹 | 根據(jù)key進(jìn)行自動排序;每個(gè)key只能出現(xiàn)一次;由于紅黑樹的平衡性,查找、插入和刪除的時(shí)間復(fù)雜度均為O(logn);不支持高效的隨機(jī)訪問。 |
std::unordered_map | 哈希表 | 不會自動排序;每個(gè)key只能出現(xiàn)一次;查找、插入和刪除的時(shí)間復(fù)雜度為O(1),支持高效的隨機(jī)訪問。 |
std::multimap | 紅黑樹 | 根據(jù)key自動排序;每個(gè)key支持對應(yīng)多個(gè)value,即可以插入多個(gè)key一樣的鍵值對;查找、插入和刪除的時(shí)間復(fù)雜度為O(logn)。 |
std::unorder_multimap | 哈希表 | 不會自動排序;支持每個(gè)key對應(yīng)對個(gè)value;操作的時(shí)間復(fù)雜度為O(1)。 |
4. JSON指針和補(bǔ)丁
該庫還支持JSON指針,是一種作為尋址結(jié)構(gòu)化值的替代方法。并且,JSON補(bǔ)丁(Patch)允許描述2個(gè)JSON值之間的差異。這兩點(diǎn)我覺得非常不錯(cuò),有助于在版本迭代時(shí)進(jìn)行合并配置文件,直接看代碼:
//原始json對象 json j_1 = R"({ "baz": ["one", "two", "three"], "foo": "bar" })"_json; std::cout << "j_1:" << j_1.dump(4) << std::endl; std::cout << j_1["/baz/1"_json_pointer] << std::endl; //補(bǔ)丁也是一個(gè)json對象 json j_patch = R"([ { "op": "replace", "path": "/baz", "value": "boo" }, { "op": "add", "path": "/hello", "value": ["world"] }, { "op": "remove", "path": "/foo"} ])"_json; std::cout << "j_patch:" << j_patch.dump(4) << std::endl; //合并補(bǔ)丁 json j_result = j_1.patch(j_patch); std::cout << "j_result:" << j_result.dump(4) << std::endl; //計(jì)算出差值補(bǔ)丁,差值是第一個(gè)參數(shù)如何操作成為第二個(gè)參數(shù)的差值 json j_diff = json::diff(j_result, j_1); std::cout << "j_diff:" << j_diff.dump(4) << std::endl; //使用插值進(jìn)行合并 json j_result_1 = j_result.patch(j_diff); std::cout << "j_result_1:" << j_result_1.dump(4) << std::endl; //輸出結(jié)果 j_1:{ "baz": [ "one", "two", "three" ], "foo": "bar" } "two" j_patch:[ { "op": "replace", "path": "/baz", "value": "boo" }, { "op": "add", "path": "/hello", "value": [ "world" ] }, { "op": "remove", "path": "/foo" } ] j_result:{ "baz": "boo", "hello": [ "world" ] } j_diff:[ { "op": "replace", "path": "/baz", "value": [ "one", "two", "three" ] }, { "op": "remove", "path": "/hello" }, { "op": "add", "path": "/foo", "value": "bar" } ] j_result_1:{ "baz": [ "one", "two", "three" ], "foo": "bar" }
上面代碼很容易理解,首先就是json指針的使用j_1["/baz/1"_json_pointer]
,在這里和前面_json
的自定義操作符一樣,_json_pointer
也是對字符串的自定義操作,其中通過/
符號來找到j(luò)son對象中深層次的內(nèi)容。
接著就是補(bǔ)丁,補(bǔ)丁自己也是一個(gè)json對象,每個(gè)操作對應(yīng)一個(gè)對象,分別是op
表示操作,path
是json指針,表示需要操作的地方,value
就是新的值。調(diào)用json::patch
方法就可以把補(bǔ)丁合并,生成一個(gè)新的json對象,可以看上面例子中的j_result
對象。
最后就是json::diff
方法,它是用來計(jì)算2個(gè)json對象的差值,生成補(bǔ)丁。傳遞進(jìn)該函數(shù)的2個(gè)json對象,補(bǔ)丁就是第一個(gè)json對象到第二個(gè)json對象的補(bǔ)丁,所以在上面例子中,我們對j_result
合并j_diff
補(bǔ)丁,又可以回到最開始的json對象。
或許你可能覺得json指針有點(diǎn)太難用了,在前面我們也看見了,其實(shí)patch
也是一個(gè)json對象,所以該庫還支持直接使用json對象來進(jìn)行合并補(bǔ)丁,這種場景非常適合配置文件的迭代,比如下面代碼:
//多了一個(gè)配置項(xiàng)head,已有的配置項(xiàng)的值為默認(rèn)值 json j_new_config = { {"name", "modbus"}, {"config",{ {"type", ""}, {"startIndex", 0}} }, {"head", 10} }; std::cout << "j_new_config:" << j_new_config.dump(4) << std::endl; //舊的配置項(xiàng),已經(jīng)有值了 json j_old_config = { {"name", "modbus"}, {"config", { {"type", "floatlh"}, {"startIndex", 17}} } }; std::cout << "j_old_config:" << j_old_config.dump(4) << std::endl; j_new_config.merge_patch(j_old_config); std::cout << "result:" << j_new_config.dump(4) << std::endl; //輸出結(jié)果 j_new_config:{ "config": { "startIndex": 0, "type": "" }, "head": 10, "name": "modbus" } j_old_config:{ "config": { "startIndex": 17, "type": "floatlh" }, "name": "modbus" } result:{ "config": { "startIndex": 17, "type": "floatlh" }, "head": 10, "name": "modbus" }
在上面代碼中,假如j_old_config
是已經(jīng)運(yùn)行的配置項(xiàng),而j_new_config
是這次版本升級后的新的配置項(xiàng),其中多了一個(gè)字段head
,且其他配置項(xiàng)都是默認(rèn)值,經(jīng)過把舊的配置文件合并到新的配置文件中,我們可以看到最終合并后的配置文件,即含有head
字段,也有舊的配置,這樣就完成了配置文件的升級。
5. 任意類型轉(zhuǎn)換
在前面說過,對于支持的類型可以隱式的轉(zhuǎn)換為json中的值,但是當(dāng)從json值獲取值時(shí),不建議使用隱式轉(zhuǎn)換,建議使用顯示的方式。比如下面代碼:
//推薦寫法 std::string s1 = "Hello World"; json js = s1; auto s2 = js.template get<std::string>(); //不推薦寫法 std::string s3 = js;
這里有一個(gè)寫法是template get<std::string>()
,其實(shí)也就是模板成員函數(shù)調(diào)用的寫法。
5.1 直接寫法
我們研究json序列化庫的最終目的是想把任何類型都可以進(jìn)行序列化和反序列化,通過前面的學(xué)習(xí),我們可以大概知道如何把任意一個(gè)類類型轉(zhuǎn)成json對象,以及從json對象轉(zhuǎn)變?yōu)轭愵愋蛯ο?。直接看代碼:
Student s = {"jack", 18}; //類類型對象轉(zhuǎn)換為json對象 json j; j["name"] = s.name; j["age"] = s.age; //json對象轉(zhuǎn)換為類類型對象 Student s1 {j["name"].template get<std::string>(), j["age"].template get<int>()};
這里我們定義一個(gè)Student
類型,通過前面所學(xué)的json操作,很容易寫出這樣代碼,但是這種代碼有點(diǎn)冗余。
5.2 from_json和to_json
其實(shí)我們可以把序列化和反序列化的操作寫在類中,也就是讓該類擁有了該能力,這種寫法如下:
#ifndef STUDENT_H #define STUDENT_H #include <string> #include "json.hpp" #include <iostream> using json = nlohmann::json; struct Student { std::string name; int age; }; void to_json(json& j, const Student& s) { j = json{ {"name", s.name}, {"age", s.age} }; } void from_json(const json& j, Student& s) { j.at("name").get_to(s.name); j.at("age").get_to(s.age); } #endif // STUDENT_H
直接在定義類的頭文件中多定義2個(gè)方法,分別為to_json
和from_json
,然后使用如下:
Student s = {"jack", 18}; //類類型對象轉(zhuǎn)換為json對象 json j = s; std::cout << "j:" << j.dump(4) << std::endl; //json對象轉(zhuǎn)換為類類型對象 auto s2 = j.template get<Student>();
這里也是非常容易理解,當(dāng)調(diào)用json的構(gòu)造函數(shù),參數(shù)是自定義類型時(shí),就會調(diào)用to_json
方法;類似的,當(dāng)調(diào)用template get<Type>()
或者get_to(Type)
時(shí),這個(gè)from_json
就會被調(diào)用。這里有幾點(diǎn)需要注意:
- 這些方法必須是公有的命名空間或者類型的命令空間,否則庫無法定位它們。
- 這些方法必須是可訪問的,不能是私有的等。
- 函數(shù)參數(shù)必須要注意,從上面例子可以看出,否則無法自動定位它們。
- 自定義的類型必須有且可以默認(rèn)構(gòu)造。
我們仔細(xì)思考一下這2個(gè)方法,其中to_json
是根據(jù)類對象構(gòu)造json對象,在前面我們說了很多。但是from_json
可能就會有問題,比如json對象中缺少一些key,這時(shí)就會報(bào)錯(cuò),因?yàn)樵L問不到,比如下面代碼:
json j = { {"name", "jack"} }; //json對象轉(zhuǎn)換為類類型對象 auto s2 = j.template get<Student>();
這個(gè)j對象就沒有age,然后調(diào)用from_json
時(shí)就會出錯(cuò),這里會直接拋出異常,有沒有其他辦法不拋出異常呢?還是有的,可以通過value
方法進(jìn)行:
void from_json(const json& j, Student& s) { s.name = j.value("name", ""); s.age = j.value("age", 0); }
當(dāng)json對象中沒有某個(gè)鍵時(shí),可以通過該方法設(shè)置一個(gè)默認(rèn)值。
5.3 使用宏
上面代碼可能還不夠簡潔,這里可以更加容易,如果想序列化后的字段和原來類類型字段一樣,可以使用宏來默認(rèn)實(shí)現(xiàn),這里有2個(gè)宏,一個(gè)是NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE
用于class或者struct對象其成員都是public的情況,還可以使用有侵入式的NLOHMANN_DEFINE_TYPE_INTRUSIVE
來訪問private成員。使用宏的話,整個(gè)使用就非常簡單了,測試代碼如下:
struct Student { std::string name; int age; NLOHMANN_DEFINE_TYPE_INTRUSIVE(Student, name, age) }; struct Teacher { std::vector<Student> students; std::string name; std::string subject; NLOHMANN_DEFINE_TYPE_INTRUSIVE(Teacher, students, name, subject) }; //使用 Student s1 {"zs", 11}; Student s2 {"ls", 13}; Teacher t; t.name = "wang"; t.subject = "math"; t.students.push_back(s1); t.students.push_back(s2); json j = t; std::cout << "j:" << j.dump(4) << std::endl; Teacher t1 = j.template get<Teacher>();
只需要在宏里面寫好類名,以及需要序列化的成員即可。
5.4 枚舉類
在自定義類型的序列化中,枚舉類型需要額外關(guān)注,默認(rèn)情況下枚舉會被序列化為int值,因?yàn)槊杜e本身保存的也就是int值。但是,在序列化和反序列化中,這種邏輯可能出現(xiàn)問題。比如現(xiàn)在有枚舉類如下:
enum TaskState{ TS_STOPPED, //0 TS_RUNNING, //1 TS_COMPLETED, //2 TS_INVALID = -1 };
這里我們把TS_INVALID賦值為-1,表示無效,其他枚舉值會按照規(guī)范依次被賦值為0、1和2,不論我們打印還是默認(rèn)序列化,TS_STOPPED的值都是0:
std::cout << TS_STOPPED << std::endl; //輸出結(jié)果 0
假如后面項(xiàng)目變化,需要新增一種枚舉,如下:
enum TaskState{ TS_TEMP, //0 TS_STOPPED, //1 TS_RUNNING, //2 TS_COMPLETED, //3 TS_INVALID = -1 };
這時(shí)TS_TEMP就會變成0,假如我們有一個(gè)舊對象序列化后的json保存在文件里,舊的json中保存的還是0,經(jīng)過反序列化后0會被反序列化為TS_TEMP,而不是預(yù)期的TS_STOPPED了,這就是默認(rèn)使用int作為枚舉值的弊端。
在該庫中,我們可以更加精確地指定給定枚舉如何映射到j(luò)son以及如何從json映射,使用NLOHMANN_JSON_SERIALIZE_ENUM
宏,代碼如下:
enum TaskState{ TS_STOPPED, //0 TS_RUNNING, //1 TS_COMPLETED, //2 TS_INVALID = -1 }; NLOHMANN_JSON_SERIALIZE_ENUM(TaskState, { {TS_INVALID, nullptr}, {TS_STOPPED, "stopped"}, {TS_RUNNING, "running"}, {TS_COMPLETED, "completed"} })
該宏就是可以聲明在to_json()
和from_json()
時(shí)枚舉所對應(yīng)的字符串,這樣不使用默認(rèn)的int來保存,就大大提高了程序的穩(wěn)定性:
json j = TS_STOPPED; assert(j == "stopped"); json j3 = "running"; assert(j3.template get<TaskState>() == TS_RUNNING);
上述代碼可以正常運(yùn)行,說明TS_RUNNING
在序列化時(shí)變成了running
,假如我們新增了一種枚舉,只要使用宏包括進(jìn)來:
enum TaskState{ TS_TEMP, TS_STOPPED, //0 TS_RUNNING, //1 TS_COMPLETED, //2 TS_INVALID = -1 }; NLOHMANN_JSON_SERIALIZE_ENUM(TaskState, { {TS_INVALID, nullptr}, {TS_STOPPED, "stopped"}, {TS_RUNNING, "running"}, {TS_COMPLETED, "completed"}, {TS_TEMP, "temp"} })
上述代碼依舊可以執(zhí)行成功,不會出現(xiàn)反序列化錯(cuò)誤的情況。
這里有一點(diǎn)需要特別注意,就是在宏NLOHMANN_JSON_SERIALIZE_ENUM
的定義中,我們把默認(rèn)的無效枚舉TS_INVALID定義在第一個(gè),這個(gè)是有特殊意義的,假如json中的值未定義,無法反序列化為任何一種枚舉,就會被反序列化為這個(gè)默認(rèn)值,代碼如下:
json jPi = 3.14; assert(jPi.template get<TaskState>() == TS_INVALID);
上面的3.14
屬于未定義的枚舉值,在這種情況下會默認(rèn)反序列化為默認(rèn)值。
以上就是C++開源庫nlohmann/json的介紹和使用詳解的詳細(xì)內(nèi)容,更多關(guān)于C++ nlohmann/json的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C語言中atoi函數(shù)模擬實(shí)現(xiàn)詳析
atoi函數(shù)功能是將數(shù)字字符串轉(zhuǎn)換為整數(shù),比如數(shù)字字符串"12345"被atoi轉(zhuǎn)換為12345,數(shù)字字符串"-12345"被轉(zhuǎn)換為-12345,下面這篇文章主要給大家介紹了關(guān)于C語言中atoi函數(shù)模擬實(shí)現(xiàn)的相關(guān)資料,需要的朋友可以參考下2022-10-10C語言數(shù)據(jù)結(jié)構(gòu)中二分查找遞歸非遞歸實(shí)現(xiàn)并分析
這篇文章主要介紹了C語言數(shù)據(jù)結(jié)構(gòu)中二分查找遞歸非遞歸實(shí)現(xiàn)并分析的相關(guān)資料,需要的朋友可以參考下2017-03-03C++中如何將operator==定義為類的成員函數(shù)
這篇文章主要介紹了C++中如何將operator==定義為類的成員函數(shù),具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-01-01QT基于TCP實(shí)現(xiàn)文件傳輸系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了QT基于TCP實(shí)現(xiàn)文件傳輸系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08