Отображение данных в формате json на структуру C++

В идеале хотелось бы определить структуру С++


struct Person {
 std::string name;
 int age;
 bool student;
} person;

передать экземпляр person в метод отображения вместе с данными json_data


map_json_to_struct(person, json_data)

после чего просто пользоваться заполненной структурой


std::cout << person.name << " : " << person.age;

StructMapping пытается решить эту задачу.


UPD На данный момент библиотека существенно обновлена, и все, показанное здесь, (кроме основной идеи) уже не актуально. Все изменения отражены во второй статье Отображение данных в формате json на структуру c++ и обратно (работа над ошибками)


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


Реализация сценария со структурой Person


#define MANAGED_STRUCT_NAME Person // определяем макрос, который задает имя
                                   // структуры
BEGIN_MANAGED_STRUCT               // определяем начало структуры

MANAGED_FIELD(std::string, name)   // определяем поле с типом 'std::string'и именем
                                   // 'name'
MANAGED_FIELD(int, age)            // определяем поле с типом 'int' и именем 'age'
MANAGED_FIELD(bool, student)       // определяем поле с типом 'bool' и именем
                                   // 'student'

END_MANAGED_STRUCT                 // определяем конец структуры
#undef MANAGED_STRUCT_NAME         // убираем макрос, который задавал имя структуры,
                                   // чтобы не было варнингов о переопределении
                                   // макроса в дальнейшем

создаем экземпляр


Person person;

задаем json данные


std::istringstream json_data(R"json(
{
 "name": "Jeebs",
 "age": 42,
 "student": true
}
)json");

передаем экземпляр person в метод отображения вместе с данными json


struct_mapping::mapper::map_json_to_struct(person, json_data);

пользуемся


std::cout << person.name << " : " << person.age;

Полностью код выглядит так
#include <iostream>
#include <sstream>

#include "struct_mapping/struct_mapping.h"

#define MANAGED_STRUCT_NAME Person
BEGIN_MANAGED_STRUCT

MANAGED_FIELD(std::string, name)
MANAGED_FIELD(int, age)
MANAGED_FIELD(bool, student)

END_MANAGED_STRUCT
#undef MANAGED_STRUCT_NAME

int main() {
 Person person;

 std::istringstream json_data(R"json(
  {
   "name": "Jeebs",
   "age": 42,
   "student": true
  }
 )json");

 struct_mapping::mapper::map_json_to_struct(person, json_data);

 std::cout <<
  person.name << " : " <<
  person.age << " : " <<
  std::boolalpha << person.student <<
  std::endl;
}

Дополнительные типы полей


Кроме простых типов (логического типа, целочисленные, с плавающей точкой и строки) поле структуры может быть так же структурой


MANAGED_FIELD_STRUCT(тип поля, имя поля)

Например так
#define MANAGED_STRUCT_NAME President <-- определяем структуру President
BEGIN_MANAGED_STRUCT

MANAGED_FIELD(std::string, name)
MANAGED_FIELD(double, mass)

END_MANAGED_STRUCT
#undef MANAGED_STRUCT_NAME

#define MANAGED_STRUCT_NAME Earth
BEGIN_MANAGED_STRUCT

MANAGED_FIELD_STRUCT(President, president) <-- определяем поле с типом President

END_MANAGED_STRUCT
#undef MANAGED_STRUCT_NAME

или массивом


MANAGED_FIELD_ARRAY(тип элемента массива, имя поля)

размерность массивов можно увеличивать


MANAGED_FIELD_ARRAY(MANAGED_ARRAY(MANAGED_ARRAY(std::string)), planet_groups)

Пример определения структуры с массивами
#define MANAGED_STRUCT_NAME MiB
BEGIN_MANAGED_STRUCT

MANAGED_FIELD_ARRAY(std::string, friends)
MANAGED_FIELD_ARRAY(MANAGED_ARRAY(std::string), alien_groups)
MANAGED_FIELD_ARRAY(MANAGED_ARRAY(MANAGED_ARRAY(std::string)), planet_groups)

END_MANAGED_STRUCT
#undef MANAGED_STRUCT_NAME

Как это работает


Основная задача — отобразить данные в формате json на структуру с++. Структура с++ — это набор полей. Каждое поле имеет имя и содержит значение определенного типа. Поэтому на структуру с++ отображаются json объекты. В структуре с++ типы полей соответствуют типам json значений и могут быть следующими:


  • bool — хранит json значение true или false
  • integral или floating point — хранит json число
  • std::string — хранит json строку
  • управляемая структура — хранит json объект
  • управляемый массив — хранит json массив

Для решения задачи используются парсер json и управляемые структуры.


Парсер json


При создании экземпляра парсера ему в конструкторе передается несколько функций, которые парсер будет вызывать процессе работы:


функция назначение
set_bool для установки логического значения
set_integral для установки целочисленного значения
set_floating_point для установки значения с плавающей точкой
set_string для установки строкового значения
start_struct для начала json объекта
end_struct для конца json объекта
start_array для начала json массива
end_array для конца json массива

Например для json данных


 {
  "price": 273,
  "author": {
   "name": "bk192077"
  },
  "chapters": [
   "launch",
   "new horizons"
  ]
 }

будут выполнена следующая последовательность вызовов


start_struct("")
set_integral("price", 273)
start_struct("author")
set_string("name", "bk192077")
end_struct()
start_array("chapters")
set_string("", "launch")
set_string("", "new horizons")
end_array()
end_struct()

Управляемые структуры


Общими словами можно сказать, что каждая управляемя структура имеет переменную (use_name), хранящую имя используемого поля. Если use_name не пустая, то она хранит имя поля, которому будут транслироваться события от парсера. Изначально use_name пустая (используемых полей нет).


  • если при парсинге встречается начало json объекта или json массива, то:
    • если use_name пустая, то в нее помещается имя поля, парсинг которого был начат
    • если use_name не пустая, то событие транслируется полю, имя которого содержится в use_name
  • если при парсинге встречается конец json объекта или json массива, то:
    • если use_name пустая, то ничего не происходит
    • если use_name не пустая, то событие транслируется полю, имя которого содержится в use_name. Если после трансляции события получен признак завершения цепочки использования, то use_name очищается
  • если при парсинге встречается установка значения, то:
    • если use_name пустая, то значение устанавливается для поля текущего экземпляра структуры по имени этого поля (а в случае массива значение добавляется в массив)
    • если use_name не пустая, то событие транслируется полю, имя которого содержится в use_name

Для этого управляемые структуры содержат несколько служебных функций


void set(std::string const &, bool) {...}
void set(std::string const &, long long) {...}
void set(std::string const &, double) {...}
void set(std::string const &, std::string const &) {...}
void use(std::string const &) {...}
bool release() {...}

Фактически эти функции вызываются парсером в следующем соответствии


парсер управляемая структура
set_bool set(std::string const &, bool)
set_integral set(std::string const &, long long)
set_floating_point set(std::string const &, double)
set_string set(std::string const &, std::string const &)
start_struct use(std::string const &)
end_struct release()
start_array use(std::string const &)
end_array release()

Например, для Person


функции set (почти одинаковы по реализации и перегружены по типу устанавливаемого значения)


void set(std::string const & field_name, bool value) {
 if (use_name.empty()) {
  // установка значения поля непосредственно у данного экземпляра структуры
  Fs_set_field<std::function<void(Person&, bool)>>::fs[field_name](*this, value);
 } else {
  // трансляция вызова полю, которое отмечено как используемое
  Fs_set<std::function<void(Person&, std::string const &, bool)>>
   ::fs[use_name](*this, field_name, value);
 }
}

функция use (после этого все вызовы будут транслироваться полю field_name)


void use(std::string const & field_name) {
 if (use_name.empty()) {
  // поле field_name становится используемым
  use_name = field_name;
 } else {
  // вызов транслируется полю use_name
  Fs_use<std::function<void(Person&, std::string const &)>>
   ::fs[use_name](*this, field_name);
 }
}

функция release (из цепочки использования удаляется последний элемент)


bool release() {
 // это была последняя структура в цепочке использования
 if (use_name.empty()) return true;

 if (Fs_release<std::function<bool(Person&)>>::fs[use_name](*this)) {
  use_name.clear();
 }

 // это была не последняя структура в цепочке использования
 return false;
}

Поля управляемой структуры инициализируются значением по умолчания, которое возвращает функция инициализации. Перед возвратом значения эта функция выполняет дополнительные действия, зависящие от типа поля.


для простых типов


регистрируется функция, которая выполняет установку значения поля по имени этого поля. Например, для структуры Person и поля age типа int


(такой код будет после подстановки макроса)


int age = [] {
 using value_type =
  std::conditional_t<std::is_same_v<bool, bool>, bool,
   std::conditional_t<std::is_same_v<std::string, bool>, std::string const &,
    std::conditional_t<std::is_floating_point_v<bool>, double, long long>>>;
     Fs_set_field<std::function<void(Person&, value_type)>>::add(
  "age",
  [] (Person & o, value_type value) {
   o.age = static_cast<bool>(value);
  });

 using USING_bool = bool;
 return USING_bool{};
}();

выражение


o.age = static_cast<bool>(value);

выполняет непосредственно установку значения для конкретного экземпляра структуры. В таком виде оно используется в gcc, для clang используется


bool Person::*p= &Person::age;
o.*p = static_cast<bool>(value);     

(различные варианты присутствуют, потому что clang не поддерживает первый вариант, а gcc падает по ICE на втором варианте)


для структур и массивов


регистрируются шесть функций, каждая из которых просто вызывает такую же функцию у структуры, в которой определяется поле. Например, для структуры Earth и поля president типа President


President president = [] {
 Fs_set<std::function<void(Earth&, std::string const &, bool)>>::add(
  "president",
  [] (Earth & o, std::string const & field_name, bool value) {
   o.president.set(field_name, value);
  });

 Fs_set<std::function<void(Earth&, std::string const &, double)>>::add(
  "president",
  [] (Earth & o, std::string const & field_name, double value) {
   o.president.set(field_name, value);
 });

 Fs_set<std::function<void(Earth&, std::string const &, long long)>>::add(
  "president",
  [] (Earth & o, std::string const & field_name, long long value) {
   o.president.set(field_name, value);
  });

 Fs_set<std::function<void(Earth&, std::string const &, std::string const &)>>::add(
  "president",
  [] (Earth & o, std::string const & field_name, std::string const & value) {
   o.president.set(field_name, value);
  });

 Fs_use<std::function<void(Earth&, std::string const &)>>::add(
  "president",
  [] (Earth & o, std::string const & name) {
   o.president.use(name);
  });

 Fs_release<std::function<bool(Earth&)>>::add(
  "president",
  [] (Earth & o) {
   return o.president.release();
  });

 using USING_President = President;
 return USING_President{};
}();

для clang, опять же, вместо


o.president.set(field_name, value);

используется


President Earth::*p = &Earth::president;
auto& pp = o.*p;
pp.set(field_name, value);

как все будет работать на примере следующего кода


#include <sstream>

#include "struct_mapping/struct_mapping.h"

#define MANAGED_STRUCT_NAME Author
BEGIN_MANAGED_STRUCT

MANAGED_FIELD(std::string, name)

END_MANAGED_STRUCT
#undef MANAGED_STRUCT_NAME

#define MANAGED_STRUCT_NAME Book
BEGIN_MANAGED_STRUCT

MANAGED_FIELD(int, price)
MANAGED_FIELD_STRUCT(Author, author)
MANAGED_FIELD_ARRAY(std::string, chapters)

END_MANAGED_STRUCT
#undef MANAGED_STRUCT_NAME

int main() {
 Book white_space;

 std::istringstream json_data(R"json(
  {
   "price": 273,
   "author": {
    "name": "bk192077"
   },
   "chapters": [
    "launch",
    "new horizons"
   ]
  }
 )json");

 struct_mapping::mapper::map_json_to_struct(white_space, json_data);
}

при компиляции


код


#define MANAGED_STRUCT_NAME Author
BEGIN_MANAGED_STRUCT

MANAGED_FIELD(std::string, name)

END_MANAGED_STRUCT
#undef MANAGED_STRUCT_NAME

определяет структуру


struct Author {
 void set(std::string const &, bool) {...}
 void set(std::string const &, std::string &) {...}
 void set(std::string const &, long long) {...}
 void set(std::string const &, double) {...}
 void use(std::string const &) {...}
 bool release() {...}

 std::string name;
};

поле name будет инициализировано пустой строкой. До этой инициализации в экземпляр класса Fs_set_field будет добавлена функция установки значения для данного поля


Fs_set_field<std::function<void(Author &, std::string const &)>>::add(
 "name",
 [] (Author & o, std::string cont & value) {
  o.name = value;
 });

код


#define MANAGED_STRUCT_NAME Book
BEGIN_MANAGED_STRUCT

MANAGED_FIELD(int, price)
MANAGED_FIELD_STRUCT(Author, author)
MANAGED_FIELD_ARRAY(std::string, chapters)

END_MANAGED_STRUCT
#undef MANAGED_STRUCT_NAME

определяет структуру


struct Book {
 void set(std::string const &, bool) {...}
 void set(std::string const &, std::string &) {...}
 void set(std::string const &, long long) {...}
 void set(std::string const &, double) {...}
 void use(std::string const &) {...}
 bool release() {...}

 int price;
 Author author;
 ManagedArray<std::string> chapters;
};

поле price будет инициализировано нулем. До этой инициализации в экземпляр класса Fs_set_field будет добавлена функция установки значения для данного поля


Fs_set_field<std::function<void(Book &, long long)>>::add(
 "price",
 [] (Book & o, long long value) {
  o.price = static_cast<int>(value);
 });

поле author будет инициализировано значением по умолчанию Author. До этой инициализации в экземпляры классов Fs_ будут добавлены функции:


Fs_set<std::function<void(Book &, std::string const &, bool)>>::add(
 "author",
 [] (Book & o, std::string const & field_name, bool value) {
  o.author.set(field_name, value);
 });

Fs_set<std::function<void(Book &, std::string const &, double)>>::add(
 "author",
 [] (Book & o, std::string const & field_name, double value) {
  o.author.set(field_name, value);
 });

Fs_set<std::function<void(Book &, std::string const &, long long)>>::add(
 "author",
 [] (Book & o, std::string const & field_name, long long value) {
  o.author.set(field_name, value);
 });

Fs_set<std::function<void(Book &, std::string const &, std::string cont &)>>::add(
 "author",
 [] (Book & o, std::string const & field_name, std::string cont & value) {
  o.author.set(field_name, value);
 });

Fs_use<std::function<void(Book &, std::string const &)>>::add(
 "author",
 [] (Book & o, std::string const & name) {
  o.author.use(name);
 });

Fs_release<std::function<bool(Book &)>>::add(
 "author",
 [] (Book & o) {
  return o.author.release();
 });

действия с полем chapters будут аналогичны действиям с полем author


при выполнении


вызов map_json_to_struct создает экземпляр парсера и запускает процедуру парсинга, в процессе чего выполняются следующие действия (map_json_to_struct фактически просто транслирует вызовы управляемой структуре, поэтому ее действия не рассматриваются):


parser managed
start_struct("") это начало самой структуры Book, поэтому map_json_to_struct не транслирует этот вызов и он просто игнорируется
set_integral("price", 273) white_space.set("price", 273)
установка значения поля price у white_space: Fs_set_field<std::function<void(Book&, long long)>>::fs["price"](white_space, 273)
start_struct("author") после этого все вызовы white_space будет транслировать полю author: white_space.use("author")
set_string:("name", "bk192077") white_space.set("name", "bk192077")
трансляция к author (вызов метода set у author): Fs_set<std::function<void(Book, std::string const &, bool)>>::fs["author"](white_space, "name", "bk192077")
установка значения поля name у author: Fs_set_field<std::function<void(Author&, std::string const &)>>::fs["name"](author, "bk192077")
end_struct() white_space.release()
трансляция к author (вызов метода release у author): Fs_release<std::function<bool(Book&)>>::fs["author"](white_space))
после этого вызовы к white_space не будут больше транслироваться к author
start_array("chapters") после этого все вызовы white_space будет транслировать полю chapters: white_space.use("chapters")
set_string:("", "launch") white_space.set("", "launch")
трансляция к chapters (вызов метода set у chapters): Fs_set<std::function<void(Book, std::string const &, bool)>>::fs["chapters"](white_space, "", "launch")
добавление в массив элемента "launch"
set_string:("", "new horizons") white_space.set("", "new horizons")
трансляция к chapters (вызов метода set у chapters): Fs_set<std::function<void(Book, std::string const &, bool)>>::fs["chapters"](white_space, "", "new horizons")
добавление в массив элемента "new horizons"
end_array() white_space.release()
трансляция к chapters (вызов метода release у chapters): Fs_release<std::function<bool(Book&)>>::fs["chapters"](white_space))
после этого вызовы к white_space не будут больше транслироваться к chapters
end_struct("") это конец самой структуры Book, поэтому map_json_to_struct не транслирует этот вызов и он просто игнорируется

В итоге


  • желаемая простота использования. В основном все сводится к определению структуры с применением набора макросов (хотя можно и вручную все прописать)
  • работает медленно, но для основного применения в качестве загрузки конфигурации и начального состояния приложения подходит (скорость на этом этапе не важна)
  • требуется компиляция с -std=c++17 В основном для:
    • if constexpr
    • static inline

Библиотека доступна на GitHub

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

    +12

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

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

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

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

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


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


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


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

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

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

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


              1. десериализовывать данные как один из сабклассов на основании каого-нибудь поля
              2. уметь в кастомные форматы (чтобы дату в ISO формате не читать как строчку)
              3. уметь десериализовывать в "плоские" сруктуры (когда некоторые поля делегируются своему полю)
              4. уметь складывать "лишние" поля которые не подошли в хэшмапу
              5. ...
            +1
            Спасибо за статью и за то, что показали свое решение. Совсем недавно я искал решение для парсинга и дампинга иерархических конфигов (вложенные классы, вектора, мапки, ...) из/в 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;
            }
            
              +1

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                  #define MANAGED_STRUCT_NAME Person
                  BEGIN_MANAGED_STRUCT  


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


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

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

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

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

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

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

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

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

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

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

                      Макросня не нужна: https://github.com/apolukhin/magic_get
                      А дальше хоть в json, хоть в xml, хоть куда.

                        0
                        Имена полей от куда брать?

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

                      Самое читаемое