Типы данных для представления JSON в С++

Я пытался понять это сейчас, и, может быть, я просто слишком долго смотрел на него?

Во всяком случае проблема заключается в том, чтобы найти хороший способ представления JSON в С++ и, прежде чем вы прочтете больше, обратите внимание, что меня не интересуют библиотеки, способные на это, поэтому я хочу сделать это в raw C или С++ (С++ 11 в порядке), нет boost, no libjson, я знаю о них и по причинам, выходящим за рамки этого вопроса, я не могу (/wont) добавлять зависимости.

Теперь, когда это прояснилось, позвольте мне рассказать вам немного о проблеме и о том, что я пробовал до сих пор.

Проблема заключается в том, чтобы найти хороший способ представления JSON в С++, причина, по которой это немного проблематично, заключается в том, что JSON является супер-слабо типизированным, а С++ действительно жестко типизирован. рассмотрите JSON на секунду, что такое JSON, действительно способный типично?

  • Число (например, 42 или 3.1415)
  • Строка (например, "my string")
  • Массив (например, [] или [1,3.1415,"my string])
  • Объект (например, {} или {42, 3.1415, "my string", [], [1,3.1415, "my string]}

Итак, это означает, что существуют два "необработанных" типа: Число и Строка, а два типа контейнеров Массив и Объект. Необработанные типы довольно прямолинейны, в то время как типы контейнеров становятся сложными на C/С++, поскольку они могут и, вероятно, будут содержать элементы разных типов, поэтому любой встроенный тип на языке не будет достаточным, как есть, массив не может содержать элементы разных типов. Это справедливо и для STL-типов (список, вектор, массив и т.д.) (Если они не имеют полиморфного равенства).

Таким образом, любой контейнер в JSON может содержать любой тип json-типа, который в значительной степени подходит для него.

Что я прототипировал или пытался и почему это не работает Моя первая наивная мысль заключалась в том, чтобы просто использовать шаблоны, поэтому я настроил тип json-object или json- node, который затем использовал бы шаблоны, чтобы решить, что в нем, поэтому у него будет структура вроде этого:

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

Хотя это казалось многообещающим, однако, когда я начал работать с ним, я понял, что столкнулся с проблемами, когда пытался упорядочить узлы в виде контейнера (например, array, vector, unordered_map и т.д.), поскольку они все еще хотите знать тип этого JSONNode! если один node определяется как JSONNode<int>, а другой - JSONNode<float> ну, тогда будет проблематично иметь их в контейнере.

Итак, я продвигаюсь мимо этого, я все равно не заинтересован в том, чтобы держать их в контейнере, я был бы счастлив сделать их самосознающими или как их называть, т.е. объявление в указателе на следующий node, но опять-таки становится сложно определить тип node, и примерно здесь, когда я начинаю думать о полиморфизме.

Полиморфизм Позвольте просто создать виртуальный JSONNode и реализовать типы JSONNumberNode, JSONStringNode, JSONArrayNode и JSONObjectNode, и они будут хорошо вписываться в любой контейнер, в котором я мог бы их использовать, используя полиморфизм, чтобы позволить им быть 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;
}
Сначала я думал, что это путь. Однако, когда я начал думать о том, как обрабатывать элемент value, я понял, что не могу получить доступ к значению, даже если я написал определенную функцию для получения значения, что бы он вернулся?

Таким образом, я уверен, что у меня есть объекты с разными типизированными значениями, но я не могу получить к ним доступ без первого приведения к правильному типу, поэтому я мог бы сделать dynamic_cast<JSONStringNode>(some_node);, но как бы я знал, к чему это можно привести? RTTI? Ну, я чувствую, что в этот момент мне становится сложнее, я думаю, я мог бы использовать typeof или decltype, выясняя, что придумать, но не удалось.

Типы POD Поэтому я попробовал что-то другое, я подумал, что могу утверждать, что, возможно, я мог бы сделать это в корме. Затем я установил бы часть value как void * и попробовал бы некоторые union отслеживать типы. Однако я получаю ту же проблему, что и у меня, а именно, как передавать данные в типы.

Я чувствую необходимость обернуть этот вопрос, почему я не углубился в то, что я пытался использовать POD..

Итак, если у кого-то есть умное решение о том, как представлять JSON в С++, учитывая эту информацию, я был бы так благодарен.

Ответ 1

Ваши последние два решения будут работать. Ваша проблема в обоих из них, по-видимому, извлекает фактические значения, поэтому давайте рассмотрим примеры. Я расскажу об идее POD по той простой причине, что использование полиморфности действительно потребует RTTI, который IMHO является уродливым.

JSON:

{
    "foo":5
}

Вы загружаете этот JSON файл, то, что вы получите, - это просто ваша обложка POD.

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

Теперь вы предполагаете, что загруженный JSON node является объектом JSON. Теперь вам придется обрабатывать две ситуации: либо это объект, либо нет. Если это не так, вы, скорее всего, попадете в состояние ошибки, поэтому могут быть использованы исключения. Но как бы вы сами извлекли объект? Ну, просто с вызовом функции.

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

Теперь, если JSON node, завернутый в wrapper, действительно является объектом 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;
        }
    }

Ответ 2

Я думаю, что вы идете в правильном направлении с помощью своего последнего подхода, но я думаю, что ему нужно изменить некоторые концепции dessigns.

Во всех анализаторах JSON, над которыми я работал до сих пор, решение о выборе типа контейнера было на стороне пользователя не на стороне анализатора, и я думаю, что это мудрое решение, почему? предположим, что у вас есть node, который содержит число в строчном формате:

{
    "mambo_number": "5"
}

Вы не знаете, хочет ли пользователь получить значение в виде строки или числа. Итак, я укажу, что JSONNumberNode и JSONStringNode не подходят для наилучшего подхода. Мой совет - создать узлы для хранения объектов, массивов и необработанных значений.

Все эти узлы будут содержать метку (имя) и список вложенных объектов в соответствии с ее основным типом:

  • JSONNode: базовый класс node, содержащий ключ и тип node.
  • JSONValueNode: тип node, который управляет и содержит необработанные значения, такие как Mambo nº5, перечисленные выше, он предоставит некоторые функции для считывания его значения, например value_as_string(), value_as_int(), value_as_long() и до сих пор...
  • JSONArrayNode: тип node, который управляет массивами JSON и содержит JSONNode accessibles по индексу.
  • JSONObjectNode: тип node, который управляет объектами JSON и содержит JSONNode, доступный по имени.

Я не знаю, хорошо ли задокументирована идея, давайте посмотрим несколько примеров:

Пример 1

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

JSON выше будет безымянным корнем JSONObjectNode, который содержит два JSONValueNode с метками name и birthYear.

Пример 2

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

JSON выше будет безымянным корнем JSONObjectNode, который содержит два JSONValueNode и один JSONArrayNode. JSONArrayNode будет содержать 8 неназванных JSONObjectNode с 8 первыми значениями последовательности Фибоначчи.

Пример 3

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

JSON выше был бы безымянным корнем JSONObjectNode, который содержит JSONObjectNode с двумя JSONValueNode с метками name и sex и одним 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, которые будут иметь тип JSONObjectNode, JSONValueNode и JSONValueNode в порядке появления, вторая JSONArrayNode - это последовательность фибоначчи, прокомментированная ранее.

Реализация

То, как я столкнулся с реализацией узлов, будет следующим:

База node будет знать свой собственный тип (значение Node, массив node или объект Node) через член 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;
};

Производные классы должны предоставить базовому типу node во время построения, значение node предоставляет пользователю преобразование сохраненного значения в тип запроса пользователя:

class JSONValueNode : JSONNode {
public:
    JSONObjectNode(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;
}

Массив node должен предоставить operator[], чтобы использовать его как массив; реализовать некоторые итераторы было бы полезно. Сохраненные значения внутреннего std::vector (выберите контейнер, который вы считаете лучшим для этой цели) были бы JSONNode.

class JSONArrayNode : JSONNode {
public:
    JSONObjectNode(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;
}

Я думаю, что Object node должен предоставить operator[] строковый ввод, потому что в С++ мы не можем реплицировать аксессуар 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 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;

Я думаю, стоит создать нулевые версии каждого объекта для предоставления в случае недопустимых acces:

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

Предпосылка: возвращает нулевой тип объекта в случае присоединения несуществующего под-объекта в объекте node или внеочередной доступ к массиву node.

Надеюсь, что это поможет.

Ответ 3

Я знаю, что вы сказали, что вас не интересуют библиотеки, но я сделал один в прошлом для декодирования/кодирования JSON с использованием С++:

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

Это довольно маленькая библиотека, которая является только заголовком, поэтому вы можете извлечь из нее мою стратегию.

По существу, у меня есть json::value, который обертывает boost::variant, поэтому он может быть либо одним из основных типов (string, number, boolean, null), либо он также может быть a array или object, конечно.

Существует несколько сложностей с форвардными объявлениями и динамическим распределением, поскольку array и object содержат value s, что в свою очередь может быть array и object s. Но эта общая идея.

Надеюсь, что это поможет.

Ответ 4

Если вы заинтересованы в обучении, я настоятельно рекомендую чтение через источник jq - это действительно чистый C-код без внешних зависимостей библиотеки json.

Внутренне jq хранит информацию о типе в простом перечислении, что устраняет большинство проблем типа компиляции. Хотя это означает, что вам необходимо создать основные операции.

Ответ 5

Я написал библиотеку для парсера JSON. Представление JSON, которое реализуется классом шаблона json::value, соответствует стандартной библиотеке С++. Это требует С++ 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. Для этого требуется соответствующая и полностью реализованная библиотека контейнеров, которая поддерживает модель распределенного распределения (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. Парсер сильно оптимизирован для скорости и низкой занимаемой памяти.

Пока я рассматриваю состояние представления С++ (json::value) по-прежнему как "Alpha", существует полная оболочка Objective-C, которая основана на основной реализации С++ (а именно, анализаторе), которая может быть рассмотрена окончательный. Однако представление С++ (json::value) все еще нуждается в некоторой работе для завершения.

Тем не менее, библиотека может быть источником ваших идей: код находится в GitHub: JPJson, особенно файлы variant.hpp и mpl.hpp в папке Source/json/utility/ и всех файлах в папке Source/json/value/ и Source/json/generator/.

Методы реализации и объем исходного кода могут быть разгромлены и были протестированы/скомпилированы только с помощью современного clang на iOS и Mac OS X - просто нужно предупредить;)

Ответ 6

Я бы реализовал упрощенную boost::variant с только 4-мя типами в ней: unordered_map, a vector, a string и (необязательно) числовой тип (нам нужна бесконечная точность?).

Каждый из контейнеров будет содержать интеллектуальные указатели на экземпляры одного и того же типа.

boost::variant хранит union по типам, которые он содержит, и enum или index asto, который имеет тип. Мы можем запросить его для индекса типа, мы можем спросить его, имеет ли он один тип i, или мы можем записать посетителя с переопределениями, которые variant отправляет правильный вызов. (последнее - apply_visitor).

Я бы подражал этому интерфейсу, потому что нашел его полезным и относительно полным. Короче говоря, повторно реализуйте часть boost, а затем используйте это. Обратите внимание, что variant является только типом заголовка, поэтому он может быть достаточно легким, чтобы просто включать.