【问题标题】:Datatypes for representing JSON in C++在 C++ 中表示 JSON 的数据类型
【发布时间】:2013-10-23 13:40:52
【问题描述】:

我一直在努力解决这个问题,也许我只是盯着它太久了?

无论如何,手头的问题是找到一种在 C++ 中表示 JSON 的好方法,在您继续阅读之前,请注意我对能够做到这一点的库不感兴趣,所以我想用原始 C 或 C++ 来做(C++11 很好),没有提升,没有 libjson 我知道它们,并且由于这个问题范围之外的原因,我不能(/不会)添加依赖项。

既然已经解决了,让我告诉你一些关于这个问题的信息,以及我到目前为止所做的尝试。

问题是要找到一种在 C++ 中表示 JSON 的好方法,这有点问题的原因是 JSON 是超级松散类型的,而 C++ 是硬类型的。考虑一下 JSON,JSON 真正能够在类型方面做什么?

  • 号码(例如423.1415
  • 字符串(例如"my string"
  • 数组(例如[],或[1,3.1415,"my string]
  • 对象(例如 {}{42, 3.1415, "my string", [], [1,3.1415, "my string]}

这意味着有两种“原始”类型,NumberString,以及两种容器类型 Array对象。原始类型相当简单,而容器类型在 C/C++ 中变得很棘手,因为它们可以并且可能包含不同类型的元素,因此语言中的任何内置类型都不够用,数组不能容纳不同类型的元素。这也适用于 STL 类型(列表、向量、数组等)(除非它们具有多态相等性)。

因此,JSON 中的任何容器都可以容纳任何类型的 json-type,这几乎是它的全部。

我的原型或尝试过的东西以及为什么它不起作用 我第一个天真的想法是只使用模板,所以我设置了一个 json-object 或 json-node 类型,然后使用模板来决定其中的内容,因此它的结构如下:

template <class T>
class JSONNode {
    const char *key;
    T value;
}

虽然这看起来很有希望,但是当开始使用它时,我意识到当我尝试将节点排序为容器类型(例如数组、向量、unordered_map 等)时遇到了麻烦,因为它们仍然想知道那个 JSONNode 的类型!如果一个节点定义为JSONNode&lt;int&gt;,而另一个节点定义为JSONNode&lt;float&gt;,那么将它们放在容器中将会有问题。

所以我超越了这一点,无论如何我对将它们保存在容器中并不是很感兴趣,我很乐意让它们具有自我意识或称之为什么,即在指向下一个节点的指针中添加广告,但是再次确定节点的类型变得很棘手,而这正是我开始思考多态性的时候。

多态性 让我们创建一个虚拟的 JSONNode 并实现一个 JSONNumberNode, JSONStringNode, JSONArrayNodeJSONObjectNode 类型,它们将很好地适合我可能希望它们放入的任何容器,使用多态性让它们都成为 JSONNode。

代码示例可能已经到位。

class JSONNode {
public:
    const char *key;
    //?? typed value, can't set a type
};

class JSONNumberNode : public JSONNode { 
public:
    int value;
}

class JSONStringNode : public JSONNode {
public:
    const char *value;
}

起初我认为这是要走的路。然而,当我开始思考如何处理值部分时,我意识到我无法访问该值,即使我编写了一个特定的函数来检索该值,它会返回什么?

所以确定我确实有具有不同类型值的对象,但是如果不首先转换为正确的类型,我就无法真正访问它们,所以我可以做一个dynamic_cast&lt;JSONStringNode&gt;(some_node);,但我怎么知道将它转换为什么? RTTI?好吧,我觉得那时它变得有点复杂,我想我可能可以使用 typeof 或 decltype 来确定将其类型转换为什么,但没有成功..

POD 类型 所以我尝试了一些不同的东西,我想争辩说,也许我实际上可以以 pod 的方式做到这一点。然后我会将value 部分设置为void * 并尝试让一些union 跟踪类型。但是,我遇到了与我已经遇到的相同的问题,即如何将数据转换为类型。

我觉得有必要解决这个问题,为什么我没有更深入地研究我使用 POD 所做的尝试......

因此,如果有人给出了如何在 C++ 中表示 JSON 的智能解决方案,我将非常感激。

【问题讨论】:

  • @Ashalynd OP 竭尽全力表示他对外部库不感兴趣。
  • @JBentley 如果纯粹用于学习目的,这个问题是正确的。如果没有,不要重新发明轮子
  • @Manu343726 无关。 OP 并没有就他是否应该重新发明轮子征求意见。他表示这正是他想做的,并正在征求有关如何做的建议。
  • boost::variant。如果你想重新发明它,但至少要检查一下设计。

标签: c++ json c++11 collections types


【解决方案1】:

我认为你的最后一种方法是朝着正确的方向前进,但我认为它需要改变一些概念设计。

到目前为止,在我工作过的所有 JSON 解析器中,选择容器类型的决定是在用户端而不是在解析器端,我认为这是一个明智的决定,为什么?假设您有一个包含字符串格式数字的节点:

{
    "mambo_number": "5"
}

您不知道用户是否希望将值作为字符串或数字检索。所以,我会指出JSONNumberNodeJSONStringNode 不适合最好的方法。我的建议是创建用于保存对象、数组和原始值的节点。

所有这些节点都将根据其主要类型包含一个标签(名称)和一个嵌套对象列表:

  • JSONNode: 基节点类,其中包含节点的键和类型。
  • JSONValueNode:管理并包含原始值的节点类型,如上面列出的 Mambo nº5,它会提供一些函数来读取其值,如 value_as_string()value_as_int()value_as_long() 等等...
  • JSONArrayNode:管理 JSON 数组并包含 JSONNodes 可按索引访问的节点类型。
  • JSONObjectNode:管理 JSON 对象并包含 JSONNodes 可按名称访问的节点类型。

我不知道这个想法是否有据可查,让我们看一些例子:

示例 1

{
    "name": "murray",
    "birthYear": 1980
}

上面的 JSON 将是一个未命名的根 JSONObjectNode,其中包含两个带有标签 namebirthYearJSONValueNode

示例 2

{
    "name": "murray",
    "birthYear": 1980,
    "fibonacci": [1, 1, 2, 3, 5, 8, 13, 21]
}

上面的 JSON 将是一个未命名的根 JSONObjectNode,其中包含两个 JSONValueNodes 和一个 JSONArrayNodeJSONArrayNode 将包含 8 个未命名的 JSONObjectNodes,其中包含斐波那契数列的前 8 个值。

示例 3

{
    "person": { "name": "Fibonacci", "sex": "male" },
    "fibonacci": [1, 1, 2, 3, 5, 8, 13, 21]
}

上面的 JSON 将是一个未命名的根 JSONObjectNode,其中包含一个 JSONObjectNode 和两个带有标签 namesexJSONValueNode,以及一个 JSONArrayNode

示例 4

{
    "random_stuff": [ { "name": "Fibonacci", "sex": "male" }, "random", 9],
    "fibonacci": [1, 1, 2, 3, 5, 8, 13, 21]
}

上面的 JSON 将是一个未命名的根 JSONObjectNode,其中包含两个 JSONArrayNode,第一个标记为 random_stuff 将包含 3 个未命名的 JSONValueNode,其类型为 JSONObjectNodeJSONValueNodeJSONValueNode 按出现顺序,第二个JSONArrayNode 是之前评论的斐波那契数列。

实施

我将面对节点实现的方式如下:

基节点将通过成员type 知道它自己的类型(值节点、数组节点或对象节点),type 的值由派生类在构造时提供。

enum class node_type : char {
    value,
    array,
    object
}

class JSONNode {
public:
    JSONNode(const std::string &k, node_type t) : node_type(t) {}
    node_type GetType() { ... }
    // ... more functions, like GetKey()
private:
    std::string key;
    const node_type type;
};

派生类必须在构造时向基类提供节点的类型,Value Node向用户提供存储的值到用户请求的类型的转换:

class JSONValueNode : JSONNode {
public:
    JSONValueNode(const std::string &k, const std::string &v) :
        JSONNode(k, node_type::value) {} // <--- notice the node_type::value
    std::string as_string() { ... }
    int as_int() { ... }
    // ... more functions
private:
    std::string value;
}

数组节点必须提供operator[] 才能将其用作数组;实现一些迭代器是值得的。内部std::vector 的存储值(选择您认为最适合此目的的容器)将为JSONNode's。

class JSONArrayNode : JSONNode {
public:
    JSONArrayNode(const std::string &k, const std::string &v) :
        JSONNode(k, node_type::array) {} // <--- notice the node_type::array
    const JSONObjectNode &operator[](int index) { ... }
    // ... more functions
private:
    std::vector<JSONNode> values;
}

我认为对象节点必须为 operator[] 提供字符串输入,因为在 C++ 中我们无法复制 JSON node.field 访问器,实现一些迭代器是值得的。

class JSONObjectNode : JSONNode {
public:
    JSONObjectNode(const std::string &k, const std::string &v) :
        JSONNode(k, node_type::object) {} // <--- notice the node_type::object
    const JSONObjectNode &operator[](const std::string &key) { ... }
    // ... more functions
private:
    std::vector<JSONNode> values;
}

用法

假设所有节点都具备所有需要的功能,我的方法的使用思路是:

JSONNode root = parse_json(file);

for (auto &node : root)
{
    std::cout << "Processing node type " << node.GetType()
              << " named " << node.GetKey() << '\n';

    switch (node.GetType())
    {
        case node_type::value:
            // knowing the derived type we can call static_cast
            // instead of dynamic_cast...
            JSONValueNode &v = static_cast<JSONValueNode>(node);

            // read values, do stuff with values
            break;

        case node_type::array:
            JSONArrayNode &a = static_cast<JSONArrayNode>(node);

            // iterate through all the nodes on the array
            // check what type are each one and read its values
            // or iterate them (if they're arrays or objects)
            auto t = a[100].GetType();
            break;

        case node_type::object:
            JSONArrayNode &o = static_cast<JSONObjectNode>(node);

            // iterate through all the nodes on the object
            // or get them by it's name check what type are
            // each one and read its values or iterate them.
            auto t = o["foo"].GetType();
            break;
    }
}

注意事项

我不会使用Json-Whatever-Node 命名约定,我更喜欢将所有内容放入命名空间并使用较短的名称;在命名空间范围之外,该名称非常易读且难以理解:

namespace MyJSON {
class Node;
class Value : Node;
class Array : Node;
class Object : Node;

Object o; // Quite easy, short and straightforward.

}

MyJSON::Node n;  // Quite readable, isn't it?
MyJSON::Value v;

我认为值得为每个对象创建 null 版本以在无效访问的情况下提供:

// instances of null objects
static const MyJSON::Value null_value( ... );
static const MyJSON::Array null_array( ... );
static const MyJSON::Object null_object( ... );

if (rootNode["nonexistent object"] == null_object)
{
    // do something
}

前提是:在访问对象节点中不存在的子对象或越界访问数组节点的情况下,返回空对象类型。

希望对你有帮助。

【讨论】:

  • 感谢您的帮助,特别是因为设计/概念和您提供了一种非常易于遵循的方法。非常感谢您抽出宝贵时间将其写得如此详细和透彻。
  • 我玩游戏迟到了,但是JSONValueNodeJSONArrayNode 类的ctor 不应该有相同的类名吗?
  • @NicholasHumphrey 你迟到了,但你完全完全正确,我有一个复制粘贴错字。我马上修。非常感谢!
  • @Paula_plus_plus 感谢您的帖子。我打算用 C++ 编写一个小的 json 解析器,你的帖子肯定有很大帮助!
  • @Paula_plus_plus 我可以问你一个问题吗?看来JSONNode 中的std::string key 是作为JSONObject 的“处理程序”使用的,这样我们就不需要使用std::unordered_map 这样的映射容器了。但是对于没有键的其他情况(即不在 JSONObject 中),您会将键保留为空字符串还是我对算法有一些误解?
【解决方案2】:

您的最后两个解决方案都可以。你在他们两个中的问题似乎是提取实际值,所以让我们看一下例子。我将介绍 POD 的想法,原因很简单,使用多态确实需要 RTTI,恕我直言,这很难看。

JSON:

{
    "foo":5
}

您加载此 JSON 文件,您将得到的只是您的 POD“包装器”。

json_wrapper wrapper = load_file("example.json");

现在您假设您加载的 JSON 节点是一个 JSON 对象。您现在必须处理两种情况:要么是对象,要么不是。如果不是,您可能最终会处于错误状态,因此可以使用异常。但是你将如何提取对象本身呢?好吧,只需一个函数调用。

try {
    JsonObject root = wrapper.as_object();
} catch(JSONReadException e) {
    std::cerr << "Something went wrong!" << std::endl;
}

现在,如果 wrapper 包裹的 JSON 节点确实是 JSON 对象,您可以继续在 try { 块中对对象执行任何操作。同时,如果 JSON “格式错误”,则进入 catch() { 块。

在内部,您可以这样实现:

class JsonWrapper {
    enum NodeType {
       Object,
       Number,
       ...
    };

    NodeType type;

    union {
        JsonObject object;
        double number
    };

    JsonObject as_object() {
        if(type != Object) {
            throw new JSONReadException;
        } else {
            return this->object;
        }
    }

【讨论】:

  • 我会使用免费函数进行向下转换,但无论如何 +1。
  • 这与我使用 POD 解决方案的方式非常相似(来自我的问题)。我没有找到一种方法来“打字”,我相信这实际上提供了一个解决方案。感谢您抽出宝贵时间阅读。
【解决方案3】:

我知道你说过你对库不感兴趣,但我过去曾做过一个使用 C++ 解码/编码 JSON 的库:

https://github.com/eteran/cpp-json

这是一个相当小的库,只有标题,所以你可以从中收集我的策略。

基本上,我有一个json::value,它包装了一个boost::variant,所以它可以是基本类型之一(stringnumberbooleannull),也可以是当然是arrayobject

前向声明和动态分配有点棘手,因为arrayobject 包含values,而arrays 和objects 又可以是arrays。但这是大意。

希望这会有所帮助。

【讨论】:

  • 谢谢,我很感激,我不反对图书馆,当它像这样小的时候,它作为参考很有帮助。
【解决方案4】:

如果你有兴趣学习,我强烈推荐reading through the jq source——它是真正干净的 C 代码,没有外部 json 库依赖。

Internally, jq keeps the type information in a simple enum,它消除了大多数编译时类型问题。虽然这确实意味着您必须建立基本操作。

【讨论】:

    【解决方案5】:

    我为 JSON 解析器编写了一个库。 JSON 表示,由模板类json::value 实现,符合 C++ 标准库。它需要 C++11 和符合标准的容器。

    JSON 值基于类 json::variant。这个与boost::variant v1.52 没有什么不同,而是使用了更现代的实现(使用可变参数模板)。这种变体实现更加简洁,尽管由于普遍应用的模板技术不是很简单。这只是一个文件,而boost::variant 的实现似乎过于复杂(确实缺少设计时的可变参数模板)。此外,json::variant 在可能的情况下利用了移动语义,并实现了一些技巧以提高性能(优化后的代码比 boost 1.53 的代码要快得多)。

    json::value 类定义了一些其他类型,表示基本类型(数字、布尔值、字符串、空值)。 Object 和 Array 容器类型将通过模板参数定义,模板参数必须是符合标准的容器。因此,基本上可以在几个标准的 lib 兼容容器中进行选择。

    最后,JSON 值包装了一个变体成员,并提供了一些成员函数和一个不错的 API,这使得使用 JSON 表示非常容易。

    该实现有一些不错的功能。例如,它支持“作用域分配器”。有了它,就可以在构建 JSON 表示时使用“Arena Allocator”来提高性能。这需要一个符合并完全实现的容器库,它支持作用域分配器模型(clang 的 std 库做到了)。但是,将此功能实现到变体类中,增加了一层额外的复杂性。

    另一个特点是,创建和访问表示非常容易。

    这是一个例子:

    #include "json/value/value.hpp"
    #include "json/generator/write_value.hpp"
    #include <iostream>
    #include <iterator>
    
    int main(int argc, const char * argv[])
    {
        typedef json::value<> Value;
    
        typedef typename Value::object_type Object;
        typedef typename Value::array_type Array;
        typedef typename Value::string_type String;
        typedef typename Value::integral_number_type IntNumber;
        typedef typename Value::float_number_type FloatNumber;
        typedef typename Value::boolean_type Boolean;
        typedef typename Value::null_type Null;
    
        Value json = Array();
        json.as<Array>().push_back("Hello JSON!");
        json.as<Array>().push_back("This is a quoted \"string\".");
        json.as<Array>().push_back("First line.\nSecond line.");
        json.as<Array>().push_back(false);
        json.as<Array>().push_back(1);
        json.as<Array>().push_back(1.0);
        json.as<Array>().push_back(json::null);
        json.as<Array>().push_back(
            Object({{"parameters",
            Object({{"key1", "value"},{"key2", 0},{"key3", 0.0}})
        }}));
    
    
        std::ostream_iterator<char> out_it(std::cout, nullptr);
        json::write_value(json, out_it, json::writer_base::pretty_print);
        std::cout << std::endl;
    
        std::string jsonString;
        json::write_value(json, std::back_inserter(jsonString));
        std::cout << std::endl << jsonString << "\n\n" << std::endl;
    }
    

    程序将以下内容打印到控制台:

    [
        "Hello JSON!",
        "This is a quoted \"string\".",
        "First line.\nSecond line.",
        false,
        1,
        1.000000,
        null,
        {
            "parameters" : {
                "key1" : "value",
                "key2" : 0,
                "key3" : 0.000000
            }
        }
    ]
    
    ["Hello JSON!","This is a quoted \"string\".","First line.\nSecond line.",false,1,1.000000,null,{"parameters":{"key1":"value","key2":0,"key3":0.000000}}]
    

    当然,还有一个解析器,它可以创建这样的json::value 表示。解析器针对速度和低内存占用进行了高度优化。

    虽然我认为 C++ 表示 (json::value) 的状态仍为“Alpha”,但有一个完整的 Objective-C 包装器,它基于 C++ 核心实现(即解析器),可以考虑最后。不过,C++ 表示 (json::value) 仍需要完成一些工作。

    不过,该库可能是您想法的来源:代码在 GitHub 上:JPJson,尤其是文件夹 Source/json/utility/ 中的文件 variant.hppmpl.hpp 以及文件夹 Source/json/value/ 和 @987654336 中的所有文件@。

    实现技术和源代码的数量可能令人心碎,并且仅在 iOS 和 Mac OS X 上使用现代 clang 进行了测试/编译 - 请注意 ;)

    【讨论】:

      【解决方案6】:

      我将实现一个简化的 boost::variant,其中只有 4 种类型:unordered_mapvectorstring 和(可选)数字类型(我们需要无限精度吗?)。

      每个容器都包含指向相同类型实例的智能指针。

      boost::variant 存储了一个union 来覆盖它所拥有的类型,以及一个enum 或索引来说明它所拥有的类型。我们可以向它询问类型索引,我们可以询问它是否有一个特定的类型,或者我们可以编写一个带有 distict 覆盖的访问者,variant 将正确的调用发送到该访问者。 (最后一个是apply_visitor)。

      我会模仿那个界面,因为我发现它既有用又相对完整。简而言之,重新实现boost 的一部分,然后使用它。请注意,variant 是仅标题类型,因此它可能足够轻,可以仅包含。

      【讨论】:

        猜你喜欢
        • 2019-09-22
        • 2013-12-08
        • 1970-01-01
        • 2012-01-17
        • 1970-01-01
        • 1970-01-01
        • 2021-03-18
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多