用于在C ++中表示JSON的数据类型

时间:2013-10-23 13:40:52

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

我一直试图想出这个问题一段时间了,也许我只是盯着它看了太长时间?

无论如何,现在的问题是找到一种在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]}

那么这意味着有两种“原始”类型,数量字符串,以及两种容器类型数组对象即可。原始类型是相当直接的,而容器类型在C / C ++中变得棘手,因为它们可以并且可能包含不同类型的元素,因此语言中的任何内置类型都不够,数组不能保持不同类型的元素。对于STL类型(列表,向量,数组等)也是如此(除非它们具有多态相等性)。

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

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

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

虽然这似乎很有希望,但是当我开始使用它时,我意识到当我尝试将节点命令为容器类型(例如数组,矢量,unordered_map等)时,我遇到了麻烦,因为它们仍然想知道那个JSONNode的类型!如果一个节点被定义为JSONNode<int>,而另一个节点被JSONNode<float>定义为好,则将它们放在容器中会有问题。

所以我越过那个,我不是那么有兴趣将它们保存在容器中,我会很高兴让它们自我意识或者称之为什么,即指向下一个节点的广告,但是再次弄清楚节点的类型也很棘手,而这就是我开始思考多态的时候。

多态性 的 让我们创建一个虚拟JSONNode并实现JSONNumberNode, JSONStringNode, JSONArrayNodeJSONObjectNode类型,它们将很好地适应我可能想要的任何容器,使用多态来让它们都是JSONNodes。

可能存在代码示例。

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<JSONStringNode>(some_node);,但我怎么知道要把它转换为什么至? RTTI?好吧,我觉得在这一点上变得有点复杂,我想我可能能够使用typeof或decltype来搞清楚要对它进行类型转换,但是还没有成功..

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

我觉得有必要回答这个问题,为什么我没有深入研究我尝试使用POD的内容。

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

6 个答案:

答案 0 :(得分:7)

你的最后两个解决方案都有效。你在他们两个中的问题似乎是提取实际值,所以让我们看看例子。我将介绍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 :(得分:7)

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

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

{
    "mambo_number": "5"
}

您不知道用户是想要将值检索为字符串还是数字。所以,我会指出JSONNumberNodeJSONStringNode不符合最佳方法。我的建议是创建用于保存对象,数组和原始值的节点。

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

  • JSONNode:基本节点类,包含密钥和节点类型。
  • JSONValueNode:管理和包含原始值的节点类型,如上面列出的Mamboº5,它将提供一些函数来读取其值,例如value_as_string()value_as_int(),{ {1}},到目前为止......
  • value_as_long():管理JSON数组并包含JSONArrayNode可访问索引的节点类型。
  • JSONNode:管理JSON对象的节点类型,包含JSONObjectNode可按名称访问的内容。

我不知道这个想法是否有详细记录,让我们看一些例子:

示例1

JSONNode

上面的JSON是一个未命名的根{ "name": "murray", "birthYear": 1980 } ,其中包含两个JSONObjectNode个标签JSONValueNodename

示例2

birthYear

上面的JSON是一个未命名的根{ "name": "murray", "birthYear": 1980, "fibonacci": [1, 1, 2, 3, 5, 8, 13, 21] } ,其中包含两个JSONObjectNode和一个JSONValueNodeJSONArrayNode将包含8个未命名的JSONArrayNode,其中包含Fibonacci序列的8个第一个值。

示例3

JSONObjectNode

上面的JSON是一个未命名的根{ "person": { "name": "Fibonacci", "sex": "male" }, "fibonacci": [1, 1, 2, 3, 5, 8, 13, 21] } ,其中包含一个JSONObjectNode,其中包含两个JSONObjectNode个标签JSONValueNodename以及一个sex 1}}。

示例4

JSONArrayNode

上面的JSON是一个未命名的根{ "random_stuff": [ { "name": "Fibonacci", "sex": "male" }, "random", 9], "fibonacci": [1, 1, 2, 3, 5, 8, 13, 21] } ,其中包含两个JSONObjectNode,第一个标记为JSONArrayNode的{​​{1}}将包含3个未命名的random_stuff类型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;
};

派生类必须向基类提供构造时间中的节点类型,值节点向用户提供将存储值转换为用户请求的类型:

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

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;
}

用法

假设所有节点都具有所有必需的功能,那么使用我的aproach的想法将是:

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
}

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

希望它有所帮助。

答案 2 :(得分:3)

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

https://code.google.com/p/cpp-json/

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

基本上,我有json::value包裹boost::variant,因此它可以是其中一种基本类型(stringnumberbooleannull)或当然也可以是arrayobject

前向声明和动态分配有一点棘手,因为arrayobject包含value s,而array s和{{object 1}}秒。但这是一般的想法。

希望这有帮助。

答案 3 :(得分:1)

如果您对学习感兴趣,我强烈推荐reading through the jq source - 它是非常干净的C代码,没有外部json库依赖。

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

答案 4 :(得分:1)

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

JSON值基于类json::variant。这个与boost::variant v1.52不同,但使用更现代的实现(使用可变参数模板)。这种变体实现更简洁,尽管由于普遍应用的模板技术并不十分简单。它只是一个文件,而boost::variant的实现似乎过于复杂(由于设计的缺乏可变参数模板)。此外,json :: variant在可能的情况下使用移动语义,并实现了一些技巧以使其变得非常高效(优化代码比boost 1.53快得多)。

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

最后,JSON值包装了一个变量成员并提供了一些很好的API,这使得使用JSON表示变得非常容易。

该实现有一些很好的功能。例如,它支持“Scoped Allocators”。有了它,在构造JSON表示时,可以使用“Arena Allocator”来提高性能。这需要一个符合且完全实现的容器库,它支持作用域分配器模型(clang的std lib执行它)。然而,将此功能实现到变体类中,增加了一整层复杂性。

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

以下是一个例子:

#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,尤其是文件夹variant.hpp中的文件mpl.hppSource/json/utility/以及所有文件在文件夹Source/json/value/Source/json/generator/

实现技术和源代码的数量可能令人头疼,并且仅在iOS和Mac OS X上使用现代clang进行测试/编译 - 只需加以警告;)

答案 5 :(得分:0)

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

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

boost::variant对其所拥有的类型存储union,并为其具有的enum或索引存储。我们可以询问它的类型索引,我们可以询问它是否有一个特定的类型,或者我们可以写一个带有干扰覆盖的访问者variant调度正确的呼叫。 (最后一个是apply_visitor)。

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