Как стать автором
Обновить

Комментарии 25

Для этой задачи кодогенерация подошла бы лучше, чем макро-ужасы.

НЛО прилетело и опубликовало эту надпись здесь

если глянуть протобуф — то там не так уж и сильно наворочено.

Ну или делать макросы чуть более вменяемыми. Как например в github.com/USCiLab/cereal
Конечно это правильно, что каждый программист должен написать свой велосипед на тему того что ему интересно и того что он использует, хотя бы что бы понимать как все это работает, но, для начала, лучше все-таки посмотреть как устроены уже готовые аналогичные решения, тем более что их уже достаточно много.
Что конкретно не так с ваши подходом:
1. Это макросы. Иногда без макросов не обойтись, но для десериализации из json они не нужны. В С++ макросы часто просачиваются через пространства имен и вызывают неожиданные конфликты.
2. Слишком многословно. По сути, все что нужно сделать для сериализации из/в json, это определить перегруженные методы для нужных классов или свободные функции. Кода будет уж точно не больше чем с макросами.
3. Слишком не гибко. Как десериализовывать уже существующие классы, например из того же STL? Подход без макросов спокойно можно обобщить на любые классы, любой степени вложенности.
4. Слишком медленно. std::function<>, в общем виде, достаточно медленный механизм, особенно если его использовать для каждого загружаемого поля.
5. Нужно что-то делать с названиями. В строчке struct_mapping::mapper::map_json_to_struct у читающего код будет рябить в глазах от map. Каждое вложенное пространство должно уточнять назначение объекта. Не обязательно повторять одно и тоже на каждом уровне.
6. Вы используете С++17 стандарт, хотя по сути ничем из него не пользуетесь.

Основная задача была в том, чтобы сделать загрузку конфигурации и начального состояния приложения из json простой для пользователя. Хотелось, чтобы использование сводилось к определению структуры без написания дополнительного кода для самого процесса. Отсюда и нет необходимости десериализовывать уже существующие классы.


В json интересными для отображения являются только логический тип, число, строка, объект и массив. Поэтому в структуре требуется существование такого же ограниченного набора типов. Использовать, например, std::list просто нет необходимости, потому что для него в json нет соответствия. Сторонние решения, которые я видел, требуют написания дополнительного кода, который связывает имена членов класса с именами json (от этого все равно никуда не уйти). Макросы позволяют убрать эту специфику за них. Сами макросы здесь ничего не "считают", они просто подставляют имена (полей и структур).


Да, от с++17 используется мало. Но отказавшись от static inline, пришлось бы добавить cpp файлы. А if constexpr заменилось бы на значительно более многословные конструкции.


Слишком медленно. std::function<>, в общем виде, достаточно медленный механизм, особенно если его использовать для каждого загружаемого поля.

согласен. мы работаем над этим

Хотелось, чтобы использование сводилось к определению структуры без написания дополнительного кода для самого процесса.
Очень сомнительное желание. С точки зрения архитектуры, почти всегда лучше отделять структуры данных от способов сериализации/десериализации. Разделив эти вещи мы можем более гибко настраивать поведение. Например, значения по умолчанию, опциональные типы, ошибки отсутствия полей объектов, в зависимости от той или иной структуры.
Использовать, например, std::list просто нет необходимости, потому что для него в json нет соответствия.
Ну как же ?! Array в json, это абстракция представляющая сериализованную последовательность элементов. Поэтому array можно десериализовывать в любую структуру поддерживающую метод push_back, в зависимости от необходимого контекста. В свою очередь, объект это абстракция ассоциативного массива со строкой в качестве ключа. Т.е. объект можно десериализовывать в любой ассоциативный контейнер с подходящими требованиями.
Сторонние решения, которые я видел, требуют написания дополнительного кода, который связывает имена членов класса с именами json (от этого все равно никуда не уйти).
На самом деле, если поменять точку зрения на проблему, и нам не важны ни гибкость, ни скорость, а важно только удобство загрузки конфигов, как в вашем случае, то никакие структуры не нужны. Вы просто на выходе получаете стандартный std::ordered_map<string, JsonObject>. По объему кода это будет одно и тоже — вместо каши из описания типов данных и сериализации, мы получим чисто сериализацию, без макросов и остального мусора.
Я не эксперт, просто мимокрокодил, но в С++ нету аналога делфишному RTTI? На нем эта задача решается легко и описывать классы особым образом не надо.
RTTI — есть, но он гораздо примитивнее и сильно ограничен, т.к. не поддерживает рефлексию. Но прелесть в том, что как раз в json она и не нужна, т.к. множество базовых типов ограничивается следующими:
— null
— bool
— number
— string
— array
— object
Все остальные типы являются производными. Таким образом рекурсивные парсеры пишутся без проблем, прямо на шаблонах без необходимости использования рефлексии.
В общем же виде, если количество возможных типов огромное (будем считать стремится к бесконечности), уже приходится придумывать собственные механизмы.

Все ещё непонятно, что делать в реальных случаях:


  1. десериализовывать данные как один из сабклассов на основании каого-нибудь поля
  2. уметь в кастомные форматы (чтобы дату в ISO формате не читать как строчку)
  3. уметь десериализовывать в "плоские" сруктуры (когда некоторые поля делегируются своему полю)
  4. уметь складывать "лишние" поля которые не подошли в хэшмапу
  5. ...
Спасибо за статью и за то, что показали свое решение. Совсем недавно я искал решение для парсинга и дампинга иерархических конфигов (вложенные классы, вектора, мапки, ...) из/в json. К моему удивлению, а не смог найти готового решения. Поэтому написал свой, как и вы.

Ваше решение «do the job», но имеет несколько недостатков, на мой взгляд:
  1. магия макросов. Это значит возможный конфликт имен (или очень длинные имена), трудность понимания этого кода и сложности для IDE
  2. определение структуры и ее парсера смешано в одном коде. Это значит, что мы не можем сделать парсер для уже готовой структуры, не можем дать значения по-умолчанию
  3. не поддерживаются std контейнеры (как я понял), сложность расширения парсера на свои типы (например std::chrono::duration)


Позволю тут оставить ссылку на свое решение: github.com/dmitryikh/rcfg

Основная идея — мы описываем парсер структуры (в том числе с вложенными полями) обычным C++ кодом. Далее использую объект парсера можно читать/писать в/из json, валидировать параметры, логировать поля, которые были обновлены.

Небольшой пример:
// Destination config
struct Config
{
    std::string dir;
    uint64_t severity;
    bool feature;
    std::string name;
    double velocity;
    std::string password;
};

// Initialize parser with rules
auto GetParser()
{
    rcfg::ClassParser<Config> p;
    p.member(&Config::dir, "Dir", rcfg::NotEmpty);
    p.member(&Config::severity, "Severity", rcfg::Bounds{0, 6}, rcfg::Default{4});
    p.member(&Config::feature, "Feature");
    p.member(&Config::name, "Name", rcfg::NotEmpty, rcfg::Default("MyName"s), rcfg::Updatable);
    p.member(&Config::velocity, "Vel", rcfg::Bounds{0.0, 100.0});
    // secret means that the field value won't be revealed after reading
    p.member(&Config::password, "Password", rcfg::NotEmpty, rcfg::Secret);
    return p;
}

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


Определение структуры и ее парсера не совсем смешаны. Именно парсер json — это отдельная
сущность, которая со структурой связана через набор колбэков и функцию map_json_to_struct. Сам парсер можно и заменить. Чего там нет — это валидации и значений по умолчанию. Однако добавить их — это хорошая идея, спасибо.

У меня похожий велосипед, но ваш мне нравится больше.

struct SessionConfig
{
    std::vector<std::string> networkConfigs;
    std::string metaFilePath;

    template< class InputOutputT >
    void serialize(InputOutputT& io);
};

template< class InputOutputT >
void SessionConfig::serialize(InputOutputT& io)
{
    io
        & tinyconf::required("networkConfigs", networkConfigs, tinyconf::nonEmpty())
        & tinyconf::optional("metaFilePath", metaFilePath, "spectra.json")
    ;
}

К моему удивлению, а не смог найти готового решения.
Вы только в стандартной библиотеке искали? Ни в boost, ни на классику не пробовали смотреть? Ну или вот?

Нет, я знаю: C++-разработчики — большие мастера изобретать велосипеды (сам такой), но, пожалуйста, оправдывайте уже чем-нибудь другим, чем «искали, не смогли найти». Глупо выглядит, когда подобных библиотек — десятки (если не сотни).

Да, C++ разработчики любят написать свой std::string, не спорю =) Думаю, это связано с любовью копанием в регистрах низкоуровневых мелочах, и отсутствием нормального менеджера зависимостей.

Насчет вашего выпада про готовые решения: библиотеки, которые вы указали — это парсеры json/ini и прочего. Условно, из json строки в map<string, any>. Это первая часть балета. Вторая часть — маппинг map<string, any> в конкретную составную структуру C++.

Именно вторую задачу решает автор статьи. Можно было бы хотя бы прочитать заголовок: «Отображение данных в формате json на структуру C++». По сути, это элементы рефлексии полей структуры с различными аттрибутами (имя поля json, дефолт и пр.).

Если знаете библиотеку, которая это делает — дайте ссылку.

EDIT: danielaparker.github.io/jsoncons имеет возможность маппинга json в структуру, но отсутствуют фичи, которые полезны для работы с конфигами (дефолтные значения, валидация параметров, логирование прочитанного конфига и пр.).
EDIT: danielaparker.github.io/jsoncons имеет возможность маппинга json в структуру, но отсутствуют фичи, которые полезны для работы с конфигами (дефолтные значения, валидация параметров, логирование прочитанного конфига и пр.).
Ну если вы хотете делать вещь для работы с конфигами, то, возможно, стоит взять Boost.Program_options и/или Boost.PropertyTree и соорудить что-то над ними?

Потому что если вы читаете конфиг, то, в общем-то, непонятно зачем вам описывать существующую структуру (и вообще какую-либо структуру), но зато ясно, что ваш конфиг-файл как-то будет связан с аргументами программы.

И при этом вам может захотеться чего-нибудь засунуть в реестр Windows, зато вам не так важна скорость работы. А при обработке запроса на нагруженный сервер — всё будет наоборот.

В общем я вот ни разу не убеждён что вот эти вот поделия имеют смысл как парсер JSON'а.

А вот если рассматривать их как работу с конфигурацией программы… там другие требования могут появиться. Например как «сливать» опции, заданные в командной строке с обычными, как поддерживать несколько файлов конфигураций (да, все эти config.d), XDG_CONFIG_HOME и прочее.

И если копать в эту сторону — то окажется что, собственно, парсер JSON'а станет в этой библиотеке небольшой (и не слишком-то важной) частью.
Маленький трюк который точно улучшит код:

#define MANAGED_STRUCT_NAME Person
BEGIN_MANAGED_STRUCT  


заменяем на
#define MANAGED_STRUCT_NAME(STRUCT_NAME) \
struct STRUCT_NAME {
using __Self = STRUCT_NAME
...


и больше не надо дефайнить промежуточную переменную
дефайнится она для использования в отладочных сообщениях. Но вы правы, оно лишнее, и алиас будет к месту.
Интересно — зачем люди нарываются в подобных случаях? Вам мало багов в ваших программах, вы ещё и через стандартную библиотеку хотите проблем огрести?
Не очень понял почему Ваш комментарий обращён ко мне, но попробую ответить: при всех минусах велосипедостроения (я бы приведённый код в продакшн не пустил) у них есть один плюс — по другому С++ не выучить. Во всяком случае я пока не встречал программистов которые бы поняли хоть что-то интересное из boost просто прочтя документацию/книгу.
Не очень понял почему Ваш комментарий обращён ко мне
Он обращён к вам потому что только в вашем примере программа лезет в запрещённые области и, соответственно, содержит undefined behavior. Причём ну вот совершенно на ровном месте и непонятно зачем.

Во всяком случае я пока не встречал программистов которые бы поняли хоть что-то интересное из boost просто прочтя документацию/книгу.
А к этому у меня особых претензий нет. Тот факт, что нормального, общепринятого, пакетного менеджера у C++ нет часто просто вынуждает строить велосипеды… и пока они не слишком велики — это как раз не страшно.

Но хорошо бы при этом, всё-таки, смотреть на стандарты и «плохому детей не учить»… а вы учите.
Дисклеймер для тех кто читает это и ещё не понял в чём проблема — не используйте двойное нижнее подчёркивание, оно зарезервировано

Ответ khim у:
Ваш первый комментарий абсолютно бесполезен, т.к. вместо того чтобы сразу указать на ошибку и сослаться на стандарт вы оставили то что оставили, да и второй комментарий тоже не очевиден о чём идёт речь пока не пройдёшь по ссылке.

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

Про то, что два подчёркивания использовать нельзя во всех виденных мною книжках написано (для примера: Beginning C++, Gentle Introduction to C++, C++ Programming for the Absolute Beginner)… хотя про подчёркивание и большую букву иногда забывают.

Потому я, разумным образом, предполагал, что раз вы такое пишите — то это сделано сознательно. Хотел понять сколько времени у вас уйдёт на то, чтобы вспомнить, что это, как бы — нехорошо.

Я показал как можно избавиться от лишнего дефайна, если копипаста этого кода где-то крашнет то будет и второй урок — не надо тупо копи-пастить код из интернета.
Не уверен, что это хороший урок. Собственно вы же и показали, почему. Особенно с учётом того, что она может не «где-то крашнуть», а вообще сделать что угодно (в зависимости от того куда и как ваш компилятор это самое __Self за'#define'ил).

Да, конечно, тянуть к себе всё, что найдено на просторах Internet не стоит — но и создавать такие «бомбы замедленного действия» в комментариях — тоже. Вы же сами и показали — почему…

P.S. Я сам обычно делаю вот так в своих проектах. Тоже от слова SELF, но без риска вызвать конфликт со стандартными #define. И запись компактная и коллизии маловероятны. В несвоих… ну там обычно есть какой-то уже свой стиль, там «как принято».
Имена полей от куда брать?
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации