Как могла бы выглядеть поддержка JSON в современном С++

в 9:22, , рубрики: api, c++, json, Блог компании Инфопульс Украина, Программирование, хранение данных, метки:

Хорошо в плане поддержки JSON живётся программистам на Javascript — по какому-то невероятному стечению обстоятельств там JSON входит в спецификацию самого языка: есть JSON — есть объект. Удобно. Неплохо дело обстоит и в языках, где JSON не входит в сам язык, но поддерживается стандартной библиотекой (Python, Ruby): импортируешь модуль — и готово.

Жизнь программистов на С++ никогда не была особо простой — поддержки JSON у нас нет ни на уровне языка, ни в стандартной библиотеке. И не будет, возможно, никогда. «Тоже мне проблему нашел!» — скажут мне опытные коллеги — «Её там и не должно быть, С++ поставляется без „батареек“. Для решения этой задачи мы...» и вот здесь они разделятся на два лагеря:

1. «Мы используем большой фреймворк (boost, Qt, POCO, другой), который применяется во всех наших проектах и умеет 150 000 разных вещей, в том числе и JSON.»
2. «Мы придерживаемся подхода в котором для каждой задачи применяется своя легковесная библиотека. В частности, для JSON мы уже 150 000 лет назад выбрали отличную библиотеку %JSON_LIB%, которая прекрасно работает.»

Да, всё так и есть. Вот только…

Чем плох подход с использованием фреймворков

Во-первых, тянуть в проект огромный фреймворк ради одного JSON — как-то уныло. Ну ладно, допустим фреймворк у вас был и так. Но тогда придётся писать работу с JSON в терминах фреймворка, а это, как правило, тихий ужас. Посмотрите, например, на документацию по JSON в Qt — куча собственных типов вроде QJsonArray, QJsonDocument, QJsonObject, QJsonValue и т.д. и их придётся использовать. О том, чтобы потом перенести код в другой проект (где этого фреймворка нет) можно сразу забыть. Ну или вот Boost: парсер JSON находится очень логично в модуле Boost.PropertyTree. Ага, так бы я и догадался. Т.е. нам предлагают плясать не от формата JSON, а от структуры данных «дерево», которая умеет себя читать в том числе и из JSON.

В общем, фреймворки навязывают нам своё виденье задачи, свой способ её решения и стремятся навсегда привязать нас к себе. Нет, если вы уверены, что нашли тот самый единственный и неповторимый фреймворк и будете с ним счастливы до конца жизни — воля ваша. Но я как-то не сторонник подобного фатализма.

Чем плох подход с использованием библиотек

Плох он вот этой частью: "...150 000 лет назад выбрали отличную библиотеку...". Скорее всего речь идёт о чём-то, что начинало писаться чуть-ли не во времена DOSа и, без сомнения, работает, но при этом, пытаясь быть совместимым со всеми платформами и стандартами языка совершенно отстаёт от прогресса. Да, всё компилируется и работает, даже тесты проходит. Но библиотека совершенно не знакома с такими вещами, как ключевое слово auto, range-based циклы, строковые литералы, raw-строки, конструкторы перемещения, списки инициализации и прочие классные вещи, делающие код одновременно более эффективным и более легко читаемым. А ведь у библиотеки, созданной годы назад, есть обязательства по обратной совместимости, а значит просто так взять и добавить это всё она не может.

Давайте немного помечтаем.

А что, если бы JSON вошел в стандартную библиотеку нового стандарта С++? Что, если бы он был написан в терминах С++1114 и без требований обратной совместимости со старыми стандартами языка? Что, если бы синтаксис этого модуля попытались бы сделать максимально приближенным к родному для JSON использованию «а-ля Javascript», но в том же время сохранить дух С++ (эффективность, минимальное потребление памяти, совместимость с STL)? Что, если бы его можно было включить в проект одним инклюдом и не беспокоиться о его сборке и линковке? Как бы это всё выглядело и работало?

И у нас есть ответ на этот вопрос! Давайте посмотрим на JSON-библиотеку для С++ написанную в соответствии со всеми этими принципами, ну и вообще написанной людьми для людей, а не чужими для хищников, как это обычно бывает.

Итак, пусть у нам нужно создать вот такой JSON-объект:

{
  "name": "Habrahabr",
  "nothing": nullptr,
  "answer": {
    "everything": 42
  },
  "companies": ["Infopulse", "TM"],
  "user": {
    "name": "tangro",
    "active": true
  }
}

Давайте возьмём и создадим!

Качаем github.com/nlohmann/json/blob/master/src/json.hpp, включаем его инклюдом в cpp-файл.

#include "json.hpp"
using json = nlohmann::json;

// создаём пустой JSON-объект
json j;
// добавляем строку, которая будет храниться как std::string
j["name"] = "Habrahabr";
// добавляем пустой вложенный объект
j["nothing"] = nullptr;
// число внутри вложенного объекта
j["answer"]["everything"] = 42;
// добавляем массив строк (будет храниться как std::vector<std::string>)
// обратите внимание - используются списки инициализации
j["companies"] = { "Infopulse", "TM" };
// добавляем ещё один объект - на этот раз используем список инилиализации с парами "ключ"-"значение"
j["user"] = { {"name", "tangro"}, {"active", true} };

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

json j2 = 
	{
	  {"name", "Habrahabr"},
	  {"nothing", nullptr},
	  {"answer", {
	    {"everything", 42}
	  }},
	  {"companies", {"Infopulse", "TM"}},
	  {"user", {
	    {"name", "tangro"},
	    {"active", true}
	  }}
	}

Так, а где же все эти модные фишки С++11?

А вот они.

// создание объекта из строкового литерала
json j = "{ "active": true, "pi": 3.141 }"_json;
// использование raw string
json j = R"(
  {
    "active": true,
    "pi": 3.141
  }
)"_json;
// использование auto
auto j = json::parse("{ "active": true, "pi": 3.141 }");
// использование range-based for
for (auto element : j) {
  std::cout << element << 'n';
}

Так, ладно, я приверженец «классического» стиля, покажите мне совместимость с STL

// создание массива, синтаксис "а-ля std::vector"
json j;
j.push_back("foo");
j.push_back(1);
j.push_back(true);
// создание объекта, синтаксис "а-ля std::map"
json o;
o["foo"] = 23;
o["bar"] = false;
// поиск элемента
if (o.find("foo") != o.end()) {
  // не найдено
}
// итерация по массиву
for (json::iterator it = j.begin(); it != j.end(); ++it) {
  std::cout << *it << 'n';
}
// перегруженный оператор [] и метод at(), поддержка std::string
const std::string tmp = j[0];
j[1] = 42;
bool foo = j.at(2);
// знакомые методы работы с контейнерами
j.size();     // 3 элемента
j.empty();    // true или false
j.type();     // json::value_t::array
j.clear();    // очищаем
// дамп в строку
std::string s = j.dump();

Ну а ещё есть поддержка создания JSON из любых STL-контейнеров (std::array, std::vector, std::deque, std::forward_list, std::list, std::set, std::multiset, std::unordered_set, std::unordered_multiset). Для линейно-упорядоченных контейнеров порядом элементов будет сохранён, для ассоциативных, понятное дело — нет.

Так, почти убедил

А ещё есть 100% покрытие библиотеки тестами и гарантия отстутствия утечек памяти от Valgrind.

Вот и всё.

В общем, я понимаю, что это «всего-лишь ещё одна библиотека для поддержки JSON» и по сути ничем она от миллиона других не отличается. Но как же всё-таки с ней удобно работать!

Автор: tangro

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js