欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

C++開源庫nlohmann/json的介紹和使用詳解

 更新時(shí)間:2023年12月30日 09:13:23   作者:yuanhao  
nlohmann/json?是一個(gè)C++實(shí)現(xiàn)的JSON解析器,使用非常方便直觀,這篇文章主要為大家詳細(xì)介紹了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
}

這里看似非常簡單,但是有一些原理我們是需要知道的。我們對比一下給定的jsonlist的結(jié)構(gòu),它是一個(gè)數(shù)組,使用[1, 0, 2],我們在jj2的構(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
  }

但是在使用對jobject的賦值如下:

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)來到jj1的區(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::vectorstd::deque、std::forward_liststd::list,其中保存值可以構(gòu)造json的值,比如int,floatboolean、字符串類型等,這些容器均可以用來構(gòu)造json數(shù)組。對于類似的關(guān)聯(lián)容器(std::setstd::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_jsonfrom_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)詳析

    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-10
  • C語言數(shù)據(jù)結(jié)構(gòu)中二分查找遞歸非遞歸實(shí)現(xiàn)并分析

    C語言數(shù)據(jù)結(jié)構(gòu)中二分查找遞歸非遞歸實(shí)現(xiàn)并分析

    這篇文章主要介紹了C語言數(shù)據(jù)結(jié)構(gòu)中二分查找遞歸非遞歸實(shí)現(xiàn)并分析的相關(guān)資料,需要的朋友可以參考下
    2017-03-03
  • C++調(diào)用Matlab函數(shù)求特征值

    C++調(diào)用Matlab函數(shù)求特征值

    這篇文章主要為大家詳細(xì)介紹了C++調(diào)用Matlab函數(shù)求特征值,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2019-06-06
  • 基于c++中的默認(rèn)拷貝函數(shù)的使用詳解

    基于c++中的默認(rèn)拷貝函數(shù)的使用詳解

    本篇文章對c++中默認(rèn)拷貝函數(shù)的使用進(jìn)行了詳細(xì)的分析介紹。需要的朋友參考下
    2013-05-05
  • C++菱形繼承和虛繼承的實(shí)現(xiàn)

    C++菱形繼承和虛繼承的實(shí)現(xiàn)

    本文主要介紹了C++菱形繼承和虛繼承的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-06-06
  • C++中如何將operator==定義為類的成員函數(shù)

    C++中如何將operator==定義為類的成員函數(shù)

    這篇文章主要介紹了C++中如何將operator==定義為類的成員函數(shù),具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-01-01
  • C++中訪問權(quán)限的示例詳解

    C++中訪問權(quán)限的示例詳解

    C++通過 public、protected、private 三個(gè)關(guān)鍵字來控制成員變量和成員函數(shù)的訪問權(quán)限(也稱為可見性),下面這篇文章主要給大家介紹了關(guān)于C++中訪問權(quán)限的相關(guān)資料,需要的朋友可以參考下
    2021-07-07
  • 用C語言實(shí)現(xiàn)一個(gè)掃雷小游戲

    用C語言實(shí)現(xiàn)一個(gè)掃雷小游戲

    這篇文章主要為大家詳細(xì)介紹了用C語言實(shí)現(xiàn)一個(gè)掃雷小游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-09-09
  • QT基于TCP實(shí)現(xiàn)文件傳輸系統(tǒng)

    QT基于TCP實(shí)現(xiàn)文件傳輸系統(tǒng)

    這篇文章主要為大家詳細(xì)介紹了QT基于TCP實(shí)現(xiàn)文件傳輸系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-08-08
  • C++中多才多藝的 const

    C++中多才多藝的 const

    在C++中,關(guān)鍵字const可以用來修飾任何作用域內(nèi)的變量、函數(shù)參數(shù)、函數(shù)本體、函數(shù)返回值、成員函數(shù)、迭代器,也可以用來修飾指針本身和指針目標(biāo),可謂多才多藝,我們要詳細(xì)了解其內(nèi)部細(xì)節(jié),以及邏輯奧秘,讓這把多功能瑞士軍刀盡情發(fā)揮其作用,需要的朋友可以參考一下
    2021-09-09

最新評論