C++開源庫nlohmann/json的介紹和使用詳解
前言
該庫的地址:github.com/nlohmann/json,到目前已經(jīng)擁有38.5K的star,可以說是被大家廣泛贊同,經(jīng)過簡單探究,確實發(fā)現(xiàn)非常不錯,可以在項目中使用。
該庫的集成非常容易,只有1個hpp文件,拿來即用,完全不需要任何復(fù)雜的編譯,尤其是剛?cè)腴T的C++開發(fā)來說,編譯一些開源庫是永遠(yuǎn)的噩夢。
同時該庫使用C++11代碼編譯,使用了不少特性,也是值得學(xué)習(xí)其原理,學(xué)習(xí)C++的不錯選擇。
下面安裝其readme,簡單介紹,以及自己的使用體驗。
1. JSON as first-class data type
中文翻譯為json作為一等數(shù)據(jù)類型,即我們可以將json數(shù)據(jù)類型作為一等公民(first-class citizen),并且支持相應(yīng)的操作和語法。
關(guān)于某種語言是否把某個特性或者功能作為一等公民看待,我個人有2點感受比較深刻。第一個是Java到Kotlin的編程語言變化,同為JVM語言,Kotlin把函數(shù)和lambda表達(dá)式看成一等公民,在Kotlin中可以將函數(shù)作為參數(shù)、作為返回值等,在Kotlin官方實例和源碼中大量使用,極大地提高代碼簡潔性。第二個是學(xué)習(xí)C++時,C++沒有把原生數(shù)組類型看成一等公民,比如數(shù)組不支持直接拷貝賦值,原因很多,比如必須固定長度,缺乏高級操作以及語法糖支持,指針語義容易混淆(C++中,數(shù)組名實際上是一個指向數(shù)組首元素的指針)等,所以推薦使用std::vector來替代原生數(shù)組。
在C++中并沒有直接將json看成一等公民,與此對應(yīng)的Python就有非常好的支持,而該庫想實現(xiàn)這一目的,就必須得提供足夠簡潔的使用方式,以及足夠豐富的操作。比如我想創(chuàng)建一個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)建一個空對象
json j;
//增加一個數(shù)字,使用double類型存儲,
j["pi"] = 3.141;
//增加一個布爾值,使用bool類型存儲
j["happy"] = true;
//增加一個字符串,使用std::string類型存儲
j["name"] = "Niels";
//通過傳遞一個nullptr增加一個空對象
j["nothing"] = nullptr;
//在對象內(nèi)部增加一個對象
j["answer"]["everything"] = 42;
//增加一個數(shù)組,使用std::vector
j["list"] = {1, 0, 2};
//增加另一個對象
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;
//運行結(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),它是一個數(shù)組,使用[1, 0, 2],我們在j和j2的構(gòu)造中,都是使用的{}來進(jìn)行賦值的,這一點就和把json看成一等公民的Python語言有較大區(qū)別,下面是Python代碼:
import sys
import os
import json
?
# 定義一個 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ù)組結(jié)構(gòu),可以直接使用[],更符合常識,C++為什么不可以,因為j["list"] = {1, 0, 2};是通過重載[]運算符對j進(jìn)行賦值,默認(rèn)的數(shù)據(jù)成員對象類型是std::vector,對std::vector的初始化使用列表初始化時,不能使用[],即如下:
//C++11可以使用列表初始化
std::vector list = {0, 1, 2};
//錯誤
std::vector list1 = [0, 1, 2];
因為該庫只能利用重載運算符等方式來讓json操作看起來像是操作json,而非C++語言支持這種操作。
搞清楚這個之后,我們再來看看object,在原始的json中,我們可以清晰地知道它對應(yīng)的類型是一個類類型:
"object": {
"currency": "USD",
"value": 42.99
}
但是在使用對j的object的賦值如下:
j["object"] = { {"currency", "USD"}, {"value", 42.99}};
這不是和前面所說的數(shù)組賦值沖突了嗎,依據(jù)列表初始化規(guī)則,這個完全可以解析為數(shù)組類型,即[["currency", "USD"],["value", 42.99]],也就是二維數(shù)組,這時我們又可以對比一下Python可以怎么寫:
import sys
import os
import json
?
# 定義一個 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,同時沒有:的語法:
//C++11可以使用列表初始化
std::map map = { {"currency", "USD"}, {"value", 42.99} };
//錯誤
std::map map1 = { {"currency":"USD"}, {"value":42.99} };
搞明白為什么之后,我們就要回答前面的問題,為什么object沒有被解析為std::vector類型呢?原因是默認(rèn)規(guī)則就是這樣的,即一層和2層{}的默認(rèn)處理邏輯是不一樣的。
假如有一些極限情況,我就是想用數(shù)組形式來保存對象格式,這時可以顯示地聲明json值的類型,使用json::array()和json::object()函數(shù):
//C++11可以使用列表初始化
std::map map = { {"currency", "USD"}, {"value", 42.99} };
//錯誤
std::map map1 = { {"currency":"USD"}, {"value":42.99} };
搞明白為什么之后,我們就要回答前面的問題,為什么object沒有被解析為std::vector類型呢?原因是默認(rèn)規(guī)則就是這樣的,即一層和2層{}的默認(rèn)處理邏輯是不一樣的。
假如有一些極限情況,我就是想用數(shù)組形式來保存對象格式,這時可以顯示地聲明json值的類型,使用json::array()和json::object()函數(shù):
//顯示聲明是一個數(shù)組,而非對象
json empty_array_explicit = json::array();
//對于沒有構(gòu)造函數(shù)或者這種,默認(rèn)隱式類型是對象
json empty_object_implicit = json({});
json empty_object_explicit = json::object();
//是一個數(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;
//運行結(jié)果
[]
{}
{}
[
[
"currency",
"USD"
],
[
"value",
42.99
]
]
搞清楚默認(rèn)類型,以及如何顯示聲明非常重要。
2. 序列化/反序列化
既然想把json打造為一等公民,序列化和反序列化是必須要具備 ,而且要可以從不同的源來進(jìn)行序列化/反序列化,比如從文件、字符串等。
2.1 與字符串
我們可以從字符串字面量來創(chuàng)建一個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;
//運行結(jié)果
{
"happy": true,
"pi": 3.141
}
{
"happy": true,
"pi": 3.141
}
"{"happy":true,"pi":3.141}"
上面代碼都是通過字符串初始化一個json對象,但是需要注意的點很多。
2.1.1 原始字符串字面量(Raw String Literal)
首先就是j2的初始化,它使用了R"()"這種語法,這種表示字符串的方式叫做原始字符串字面量。在C++中,可以使用R"()"來表示原始字符串字面量,使用原始字符串字面量可以方便地包含特殊字符(比如反斜杠、引號等)或者多行文本的字符串,而無需對這些特殊字符進(jìn)行轉(zhuǎn)移。
比如如下代碼:
{
qDebug() << "Hello\tWorld!";
qDebug() << R"(Hello\tWorld
NEW LINE)";
}
運行結(jié)果是:
可以發(fā)現(xiàn)普通字符串中\t被解釋為制表符,而在原始字符串字面量中,\t不會被解析,并且換行也被保留了下來。其實在其他語言中,使用這種方式更為簡單,比如Kotlin使用"""進(jìn)行包裹字符串,或者Python使用```進(jìn)行包裹字符串。
通過這個小知識點的學(xué)習(xí),我們明顯發(fā)現(xiàn)j2就比j的初始化方式更人性化,至少不用去在意字符串中的轉(zhuǎn)移字符。
2.1.2 自定義字面量操作符
然后我們關(guān)注點來到j和j1的區(qū)別,可以發(fā)現(xiàn)j多了一個_json的后綴,然后在輸出打印中就可以正常解析,而j1卻不可以。這里的核心點就是字符串字面量后面的_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)該想到自定義操作符,沒錯,operator ""是C++11引入的自定義字面量操作符,通過重載operator ""可以為特定的后綴自定義語義,這種機制可以使得我們可以像使用內(nèi)置的字面量(如整數(shù)、浮點數(shù)、字符串等)一樣自然地使用自定義的字面量,從而提高代碼的可讀性和表達(dá)力。
舉個簡單的例子:
//為long long類型定義一個_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
從這個例子我們來看上面_json的含義,它就是給const char*類型即字符串類型添加一個_json后綴,作用就是調(diào)用nlohmann::json::parse(s, s + n)返回一個json對象,理解了這個之后,我們也就可以理解為什么j1不能被解析,因為沒有_json后綴,根本不會調(diào)用parse函數(shù)進(jìn)行解析成json對象。
因為_json這種用法就是語法糖,所以其實想解析一個字符串成json,就可以直接調(diào)用json::parse()函數(shù):
json j3 = json::parse(R"({"happy": true,"pi": 3.141})");
從前面的錯誤用法打印來看,一般情況下我們認(rèn)為json對象要不表示一個對象,要不表示一個數(shù)組,但是從打印j1來看:
json j1 = "{"happy":true,"pi":3.141}";
std::cout << j1.dump(4) << std::endl;
//運行結(jié)果
"{"happy":true,"pi":3.141}"
這里居然返回一個字符串。所以這里一定要區(qū)分序列化和賦值的區(qū)別,對于序列化,在前面我們說了2種方式,一種是使用列表初始化,一種是使用字符串字面量,而直接賦值的話,json內(nèi)部會直接保存一個字符串。
2.2 與file
從文件中反序列化一個json對象可以說是在配置文件時非常常用了,這里使用也非常簡單,代碼如下:
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è)計一套STL風(fēng)格訪問的API也就很有必要,使用如下:
//創(chuàng)建一個數(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);
//比較運算符
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)建一個對象
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;
}
//判斷有沒有某個鍵值對
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容器,可以做個簡單的概述:
| 容器類型 | 底層實現(xiàn) | 特點 | 適用場景 |
|---|---|---|---|
| std::vector | 動態(tài)數(shù)組 | 可變大小的連續(xù)存儲空間,支持隨機訪問,尾部插入和刪除效率高。 | 需要在末尾進(jìn)行頻繁插入和刪除操作,以及隨機訪問元素的情況。 |
| std::array | 靜態(tài)數(shù)組 | 固定大小的連續(xù)的靜態(tài)數(shù)組,在編譯期就確定了大小,并且不會改變。 | 替代原生數(shù)組,有更多的成員函數(shù)與操作API。 |
| std::list | 雙向鏈表 | 支持雙向迭代器,插入和刪除元素效率高,不支持隨機訪問。 | 需要頻繁在中間位置進(jìn)行插入和刪除操作,不需要隨機訪問元素。 |
| std::deque | 雙端隊列 | 支持隨機訪問,支持在兩端插入和刪除元素,動態(tài)地分配存儲空間。 | 需要在兩端插入和刪除元素,并且需要隨機訪問元素的情況。 |
| std::set | 紅黑樹 | 自動排序元素,不允許重復(fù)元素,插入和刪除的時間復(fù)雜度為O(logN)。 | 需要自動排序且不允許重復(fù)元素的情況。 |
| std::multiset | 紅黑樹 | 自動排序元素,允許重復(fù)元素,插入和刪除的時間復(fù)雜度為O(1)。 | 需要自動排序且允許重復(fù)元素的情況。 |
| std::unordered_set | 哈希表 | 無序存儲元素,不允許重復(fù)元素,插入和查找的時間復(fù)雜度為O(1)。 | 不需要排序,并且不允許重復(fù)元素的情況。 |
| std::unordered_multiset | 哈希表 | 無序存儲元素,允許重復(fù)元素,插入和查找的時間復(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);
這幾種容器也做個概述,和std::set類似,也是從是否自動排序和重復(fù)(一鍵多值)這兩個維度來擴展,和Java還是有一點區(qū)別的,在Java中使用最多的是HashMap,類似C++的std::unordered_set:
| 容器類型 | 底層 | 特點 |
|---|---|---|
| std::map | 紅黑樹 | 根據(jù)key進(jìn)行自動排序;每個key只能出現(xiàn)一次;由于紅黑樹的平衡性,查找、插入和刪除的時間復(fù)雜度均為O(logn);不支持高效的隨機訪問。 |
| std::unordered_map | 哈希表 | 不會自動排序;每個key只能出現(xiàn)一次;查找、插入和刪除的時間復(fù)雜度為O(1),支持高效的隨機訪問。 |
| std::multimap | 紅黑樹 | 根據(jù)key自動排序;每個key支持對應(yīng)多個value,即可以插入多個key一樣的鍵值對;查找、插入和刪除的時間復(fù)雜度為O(logn)。 |
| std::unorder_multimap | 哈希表 | 不會自動排序;支持每個key對應(yīng)對個value;操作的時間復(fù)雜度為O(1)。 |
4. JSON指針和補丁
該庫還支持JSON指針,是一種作為尋址結(jié)構(gòu)化值的替代方法。并且,JSON補丁(Patch)允許描述2個JSON值之間的差異。這兩點我覺得非常不錯,有助于在版本迭代時進(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;
//補丁也是一個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;
//合并補丁
json j_result = j_1.patch(j_patch);
std::cout << "j_result:" << j_result.dump(4) << std::endl;
//計算出差值補丁,差值是第一個參數(shù)如何操作成為第二個參數(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)容。
接著就是補丁,補丁自己也是一個json對象,每個操作對應(yīng)一個對象,分別是op表示操作,path是json指針,表示需要操作的地方,value就是新的值。調(diào)用json::patch方法就可以把補丁合并,生成一個新的json對象,可以看上面例子中的j_result對象。
最后就是json::diff方法,它是用來計算2個json對象的差值,生成補丁。傳遞進(jìn)該函數(shù)的2個json對象,補丁就是第一個json對象到第二個json對象的補丁,所以在上面例子中,我們對j_result合并j_diff補丁,又可以回到最開始的json對象。
或許你可能覺得json指針有點太難用了,在前面我們也看見了,其實patch也是一個json對象,所以該庫還支持直接使用json對象來進(jìn)行合并補丁,這種場景非常適合配置文件的迭代,比如下面代碼:
//多了一個配置項head,已有的配置項的值為默認(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;
//舊的配置項,已經(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)運行的配置項,而j_new_config是這次版本升級后的新的配置項,其中多了一個字段head,且其他配置項都是默認(rèn)值,經(jīng)過把舊的配置文件合并到新的配置文件中,我們可以看到最終合并后的配置文件,即含有head字段,也有舊的配置,這樣就完成了配置文件的升級。
5. 任意類型轉(zhuǎn)換
在前面說過,對于支持的類型可以隱式的轉(zhuǎn)換為json中的值,但是當(dāng)從json值獲取值時,不建議使用隱式轉(zhuǎn)換,建議使用顯示的方式。比如下面代碼:
//推薦寫法
std::string s1 = "Hello World";
json js = s1;
auto s2 = js.template get<std::string>();
//不推薦寫法
std::string s3 = js;
這里有一個寫法是template get<std::string>(),其實也就是模板成員函數(shù)調(diào)用的寫法。
5.1 直接寫法
我們研究json序列化庫的最終目的是想把任何類型都可以進(jìn)行序列化和反序列化,通過前面的學(xué)習(xí),我們可以大概知道如何把任意一個類類型轉(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>()};
這里我們定義一個Student類型,通過前面所學(xué)的json操作,很容易寫出這樣代碼,但是這種代碼有點冗余。
5.2 from_json和to_json
其實我們可以把序列化和反序列化的操作寫在類中,也就是讓該類擁有了該能力,這種寫法如下:
#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個方法,分別為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ù)是自定義類型時,就會調(diào)用to_json方法;類似的,當(dāng)調(diào)用template get<Type>()或者get_to(Type)時,這個from_json就會被調(diào)用。這里有幾點需要注意:
- 這些方法必須是公有的命名空間或者類型的命令空間,否則庫無法定位它們。
- 這些方法必須是可訪問的,不能是私有的等。
- 函數(shù)參數(shù)必須要注意,從上面例子可以看出,否則無法自動定位它們。
- 自定義的類型必須有且可以默認(rèn)構(gòu)造。
我們仔細(xì)思考一下這2個方法,其中to_json是根據(jù)類對象構(gòu)造json對象,在前面我們說了很多。但是from_json可能就會有問題,比如json對象中缺少一些key,這時就會報錯,因為訪問不到,比如下面代碼:
json j = {
{"name", "jack"}
};
//json對象轉(zhuǎn)換為類類型對象
auto s2 = j.template get<Student>();
這個j對象就沒有age,然后調(diào)用from_json時就會出錯,這里會直接拋出異常,有沒有其他辦法不拋出異常呢?還是有的,可以通過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對象中沒有某個鍵時,可以通過該方法設(shè)置一個默認(rèn)值。
5.3 使用宏
上面代碼可能還不夠簡潔,這里可以更加容易,如果想序列化后的字段和原來類類型字段一樣,可以使用宏來默認(rèn)實現(xiàn),這里有2個宏,一個是NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE用于class或者struct對象其成員都是public的情況,還可以使用有侵入式的NLOHMANN_DEFINE_TYPE_INTRUSIVE來訪問private成員。使用宏的話,整個使用就非常簡單了,測試代碼如下:
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值,因為枚舉本身保存的也就是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
假如后面項目變化,需要新增一種枚舉,如下:
enum TaskState{
TS_TEMP, //0
TS_STOPPED, //1
TS_RUNNING, //2
TS_COMPLETED, //3
TS_INVALID = -1
};
這時TS_TEMP就會變成0,假如我們有一個舊對象序列化后的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()時枚舉所對應(yīng)的字符串,這樣不使用默認(rèn)的int來保存,就大大提高了程序的穩(wěn)定性:
json j = TS_STOPPED;
assert(j == "stopped");
json j3 = "running";
assert(j3.template get<TaskState>() == TS_RUNNING);
上述代碼可以正常運行,說明TS_RUNNING在序列化時變成了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)反序列化錯誤的情況。
這里有一點需要特別注意,就是在宏NLOHMANN_JSON_SERIALIZE_ENUM的定義中,我們把默認(rèn)的無效枚舉TS_INVALID定義在第一個,這個是有特殊意義的,假如json中的值未定義,無法反序列化為任何一種枚舉,就會被反序列化為這個默認(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語言數(shù)據(jù)結(jié)構(gòu)中二分查找遞歸非遞歸實現(xiàn)并分析
這篇文章主要介紹了C語言數(shù)據(jù)結(jié)構(gòu)中二分查找遞歸非遞歸實現(xiàn)并分析的相關(guān)資料,需要的朋友可以參考下2017-03-03
C++中如何將operator==定義為類的成員函數(shù)
這篇文章主要介紹了C++中如何將operator==定義為類的成員函數(shù),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-01-01

