Всем привет! Меня зовут Тимур, я работаю в YADRO, и уже больше двух месяцев мой системный компилятор — gcc16. В новом стандарте добавили очень много, и сегодня я расскажу вам о некоторых обновлениях. В основном о рефлексии, но также будет немного и о контрактах.Рефлексия в программировании — это способность программы работать со своим исходным кодом. Например, пройтись в цикле по всем полям класса и узнать их названия и типы. Работа с json — это очень хороший пример для изучения возможностей рефлексии. Я поигрался с ней, мне очень понравилось, и этой радостью я хочу поделиться с сообществом. Читать далее
Всем привет! Меня зовут Тимур, я работаю в YADRO, и уже больше двух месяцев мой системный компилятор — gcc16. В новом стандарте добавили очень много, и сегодня я расскажу вам о некоторых обновлениях. В основном о рефлексии, но также будет немного и о контрактах.
Рефлексия в программировании — это способность программы работать со своим исходным кодом. Например, пройтись в цикле по всем полям класса и узнать их названия и типы. Работа с json — это очень хороший пример для изучения возможностей рефлексии. Я поигрался с ней, мне очень понравилось, и этой радостью я хочу поделиться с сообществом.

Как я раньше работал с json в C++? Шел по стандартному пути и брал Python. Не шучу, за много лет программирования на C++ я не использовал json в своей работе. Недавно мне потребовалось сделать один небольшой проект за очень сжатые сроки. Надо было читать несколько toml-файлов, обрабатывать данные из них и сохранять в yaml. В последний раз до этого на Python я писал пять лет назад, но выбора у меня все равно не было.
Взял я, значит, Python и начал разбираться с парсингом json. У меня есть небольшой опыт в js, и там работа с json достаточно удобна, что неудивительно. Поэтому использование ["key"] я даже не рассматривал и искал нормальные варианты. За пару дней перепробовал разные решения и в итоге нашел pydantic. Эта библиотека предоставляет удобный способ взаимодействия с данными. Пример кода:
class FrrConfig(BaseModel):
enable: bool = True
net: str
daemons: list[FrrDaemon]
ospfd: OspfdFrrConfig | None = NoneВ коде просто указывается название переменной, тип и значение по умолчанию. Все остальное библиотека берет на себя: сама заполнит все поля и проведет валидацию данных.
После выхода C++26 я понял, что должен сделать то же самое, но на «плюсах». На данный момент есть следующий функционал:
Поддержка базовых типов json.
Можно использовать optional в C++ для хранения опциональных данных.
Json-массив можно представить разными C++-контейнерами, включая хеш-таблицы.
Значения enum отображаются в текстовое представление прямо как в коде
Для типа можно сгенерировать json-схему.
Значения можно валидировать с помощью контрактов.
Для парсинга используется библиотека от nlohmann, а подключается она через Conan. Как мне подсказали мои коллеги, simdjson уже поддерживает рефлексию и в ней можно использовать все то, что я опишу.
Работа с enumНовый стандарт позволяет автоматически переводить значение enum в его строковое представление из исходного кода. В контексте json это можно использовать для указания только определенного набора значений для строки. Для более плавного знакомства с синтаксисом рефлексии я покажу, как это можно сделать. Рассмотрим enum class:
enum class standart : std::uint8_t
{
cpp20,
cpp23,
cpp26
}Этот тип будет сериализован так:
{“a”: ”cpp20”, “b”: “cpp23”, “c”: “cpp26”}Его также можно десериализовать и получить значение типа standart.
Функция для получения строкового представления значения enum class:
/**
\brief A function for getting a text description of the enum class value
\tparam T enum class
\param [in] enum_obj enum class value
\return enum class value as string
*/
template<scoped_enum_json_type T>
std::string_view get_scoped_enum_value_name(const T& enum_obj)
{
constexpr auto members = std::define_static_array(std::meta::enumerators_of(^^T));
template for (constexpr auto& member : auto(members))
{
if constexpr (std::meta::has_identifier(member) && !std::meta::is_function(member))
{
constexpr auto member_name = std::meta::identifier_of(member);
if (std::remove_cvref_t<T>([:member:]) == std::remove_cvref_t<T>(enum_obj))
{
return member_name;
}
}
}
// Inaccessible code
return "no_enum_representation";
}Функция enumerators_of() работает с рефлексией enum и позволяет получить все значения enum в строковом представлении. Define_static_array() вернет массив, доступный на этапе компиляции. В members будут находиться все значения типа T. С помощью нового синтаксиса template for можно проитерироваться по массиву в момент компиляции!
Внутри цикла используется синтаксис if constexpr. Такая проверка выполнится в момент компиляции. С функцией identifier_of можно получить строковое представление значения enum и записать его в переменную, которая будет доступна в коде как обычный string_view. Так как member — объект рефлексии, его нельзя просто сравнить с аргументом функции. Нужно использовать сплайс для превращения рефлексии в обычный C++-объект и сравнить с аргументом функции его.
Новый синтаксис может быть запутанным и не очень понятным, поэтому рассмотрим, что конкретно произойдет в момент компиляции и, соответственно, не будет происходить во время выполнения кода. А что скомпилируется как есть и будет работать только в рантайме.
Определим простое перечисление:
enum class gender : bool
{
male,
female
};Для него цикл template for сделает две итерации — по male и female. Проверка с constexpr выполнится для обоих значений. Логику итоговой функции можно представить примерно так:
if (enum_obj == gender::male)
{
return "male";
}
if (enum_obj == gender::female)
{
return "female";
}Вся работа с полями будет происходить похожим образом.
Работа с сериализациейЯ реализовал сериализацию одним заголовочным файлом, не требующим библиотек. Для большей модульности печать в json вынесена в отдельный класс, благодаря чему для смены формата не придется менять сериализатор. Объект, который нужно сериализовать, передается функции to_json.
/**
\brief A function for serializing c++ object to json
Wrapper over object_to_json
\tparam T json object type or, which is the same thing, inheritance of the model class in c++
\tparam S a stream type that supports output operations via `operator<<`
\param [in] data the data to print
\param [out] stream stream to put json into it
\param [in] settings json output stream settings
*/
template <model_type T, ostream_type S>
void to_json(const T& data, S& stream, const json_print_settings& settings = json_print_settings())
{
json_printer<S> printer(stream, settings);
object_to_json(data, printer);
if (settings.multiline)
stream << "\n";
}Это функция-обертка, вызывающая настоящую функцию сериализации object_to_json.
/**
\brief A function for serializing c++ object to json
\tparam T json object type or, which is the same thing, inheritance of the model class in c++
\tparam S a stream type that supports output operations via `operator<<`
\param [in] object the data to print
\param [out] printer json printer object
*/
template <model_type T, ostream_type S>
void object_to_json(const T& object, json_printer<S> printer)
{
constexpr auto ns_refl = ^^T;
constexpr auto ctx = std::meta::access_context::current();
constexpr auto members = std::define_static_array(std::meta::members_of(ns_refl, ctx));
printer.open();
template for (constexpr auto& member : auto(members))
{
if constexpr (std::meta::has_identifier(member) && !std::meta::is_function(member))
{
constexpr auto member_name = std::meta::identifier_of(member);
printer.print_name(member_name);
class_member_to_json(object.[:member:], printer);
}
}
printer.close();
}Что делает эта функция:
Берет рефлексию типа с помощью ^^. Контекст позволяет ограничивать область видимости для рефлексии. Например, с помощью current указывается текущая область видимости. То есть в members попадут только те поля, что доступны из этого места в коде. Это сделано специально, чтобы скрыть непубличные члены.
Проходит в цикле по всем мемберам класса и проверяет, что у мембера есть имя и он не является функцией.
Вызывает шаблонную функцию обработки члена класса и передает в нее ссылку на поле объекта через сплайс. Изначально вместо шаблонной функции я использовал if constexpr, но это не подойдет при большом количестве возможных типов.
Определение шаблона функции сериализации объекта:
/**
\brief Defining a function for printing data from c++ classes
\tparam T interpretation of json type in c++
\tparam S a stream type that supports output operations via `operator<<`
\param [in] member the data to print
\param [out] printer json printer object
*/
template <class T, ostream_type S>
void class_member_to_json(const T& member, json_printer<S> printer);Для всех поддерживаемых типов есть своя реализация. Чтобы различать функции, между типами применяются концепты.
Функция сериализации обычного типа:
/**
\brief Implementing a function for printing basic json types from c++ classes
\tparam T one of the basic json types
\tparam S a stream type that supports output operations via `operator<<`
\param [in] member the data to print
\param [out] printer json printer object
*/
template <basic_json_type T, ostream_type S>
void class_member_to_json(const T& member, json_printer<S> printer)
{
if constexpr (std::is_same_v<T, std::string>)
printer.print_string(member);
else
printer.print_value(member);
}Концепт для этой функции определяется так:
/**
\brief A concept for defining basic json types
\tparam T must be one of the built-in integer types, a floating-point number, a string, or a boolean variable
*/
template <class T>
concept basic_json_type = std::integral<T> || std::floating_point<T> || std::is_same_v<T, std::string> || std::is_same_v<T, bool>;За работу с этими типами отвечает json библиотека, поэтому никакой дополнительной работы для них с моей стороны не проводилось.
Вложенные объекты сериализуются с помощью рекурсии:
/**
\brief Implementing a function for printing c++ models as json objects
\tparam T json object type or, which is the same thing, inheritance of the model class in c++
\tparam S a stream type that supports output operations via `operator<<`
\param [in] member the data to print
\param [out] printer json printer object
*/
template <object_json_type T, ostream_type S>
void class_member_to_json(const T& member, json_printer<S> printer)
{
object_to_json(member, printer);
}Концепт для объекта:
/**
\brief A concept for defining models
Same as model_type
\tparam T must be inherited from the model class
*/
template <class T>
concept object_json_type = std::is_base_of_v<model, T>;Я использую наследование от типа model как отличительный признак для сериализуемого типа данных. Изначально я сделал так для большего сходства с pydantic. Когда я закончил код, коллеги предложили мне несколько других вариантов: использовать аннотации и проверять их с помощью рефлексии или сделать отдельный трейт serializable.
Для контейнера все будет сложнее. Внутри контейнера могут находиться объекты или другие контейнеры. Сами контейнеры бывают разные, а для json неважно, map, list или queue. Поэтому эта функция немного сложнее:
/**
\brief Implementing a function for printing c++ containers as json arrays
\tparam T json array or, which is the same thing, c++ emplace_back or emplace container
\tparam S a stream type that supports output operations via `operator<<`
\param [in] member the data to print
\param [out] printer json printer object
*/
template <container_json_type T, ostream_type S>
void class_member_to_json(const T& member, json_printer<S> printer)
{
printer.open_container();
for (const auto& it : member)
{
printer.print_container_delimeter();
if constexpr (std::is_base_of_v<model, typename T::value_type>)
object_to_json(it, printer);
else
class_member_to_json(it, printer);
}
printer.close_container();
}Здесь в рантайм выполнится цикл по всем элементам контейнера. В момент компиляции для каждой версии шаблонной функции пройдет проверка, является ли тип внутри контейнера наследником model. Если да, то будет вызываться функция сериализации объекта, а если нет, то поля.
Концепт для определения контейнера:
/**
\brief A concept for defining a container with adding elements via emplace_back
\tparam T must be a container that supports begin(), end(), emplace_back()
*/
template <class T>
concept emplace_back_container_type = requires(T t)
{
t.begin();
t.end();
t.emplace_back();
};
/**
\brief A concept for defining a container with adding elements via emplace
\tparam T must be a container that supports begin(), end(), emplace()
*/
template <class T>
concept emplace_container_type = requires(T t)
{
t.begin();
t.end();
t.emplace();
};
/**
\brief A concept for defining containers for json
\tparam T must be one of the emplace or emplace_back containers and not optional
*/
template <class T>
concept container_json_type = (emplace_back_container_type<T> || emplace_container_type<T>) && !is_optional<T>;Этот концепт проверяет, что тип контейнера использует emplace или emplace_back для добавления нового элемента, имеет методы begin и end, а также не является std::optional.
Для обработки std::optional тоже нужна отдельная функция. Optional позволяет указывать опциональные поля, которых может не быть в объекте при сериализации и в исходных данных при десериализации.
/**
\brief Implementing a function for printing c++ optionals as json data
\tparam T optional with valid json types
\tparam S a stream type that supports output operations via `operator<<`
\param [in] member the data to print
\param [out] printer json printer object
*/
template <optional_json_type T, ostream_type S>
void class_member_to_json(const T& member, json_printer<S> printer)
{
if (member.has_value())
class_member_to_json(member.value(), printer);
else
printer.print_null();
}Функция проверяет наличие значения в optional, и, если оно есть, вызывает для него функцию обработки поля. Если optional пуст, то будет выведен null.
Концепт для optional:
template<typename> constexpr bool is_optional_impl = false;
template<typename T> constexpr bool is_optional_impl<std::optional<T>> = true;
template<typename T>
constexpr bool is_optional = is_optional_impl<std::remove_cvref_t<T>>;
/**
\brief A concept for defining optional members for json
\tparam T must be std::optional with one of the supported json types
*/
template <class T>
concept optional_json_type = is_optional<T>;Но json может быть не только простым объектом, но и массивом.
// Json из объекта
{
object
}
// Json из массива
[
array
]Поэтому для сериализации нужна еще одна функция:
/**
\brief A function for serializing c++ container to json
\tparam T container with json object type or, which is the same thing, inheritance of the model class in c++
\tparam S a stream type that supports output operations via `operator<<`
\param [in] data the data to print
\param [out] stream stream to put json into it
\param [in] settings json output stream settings
*/
template <container_json_type T, ostream_type S>
void to_json(const T& data, S& stream, const json_print_settings& settings = json_print_settings())
{
json_printer<S> printer(stream, settings);
printer.open_container();
for (const auto& it : data)
{
printer.print_container_delimeter();
object_to_json(it, printer);
}
printer.close_container();
if (settings.multiline)
stream << "\n";
}Эта функция пройдет в цикле по контейнеру и напечатает каждый элемент.
Генерация схемыЯ люблю явную типизацию, из-за этого я когда-то давно перешел с Python на C++. Насколько мне не нравится ванильный js, настолько же мне нравится типизированный ts. А Python с явным указанием типов да с хорошим lsp-сервером — просто красота. Поэтому я был обязан сделать генерацию схемы для типа.
Дам краткое описание json schema, если кто не знает, что это такое. Json schema — документ в формате json, описывающий структуру другого json. В схеме указываются все возможные поля и их типы, могут накладываться ограничения для валидации, а также указываться опциональность полей.
Простой json:
{
"firstName": "John",
"lastName": "Doe",
"age": 21
}Пример схемы для этих данных:
{
"$id": "https://example.com/person.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Person",
"type": "object",
"properties": {
"firstName": {
"type": "string",
"description": "The person's first name."
},
"lastName": {
"type": "string",
"description": "The person's last name."
},
"age": {
"description": "Age in years which must be equal to or greater than zero.",
"type": "integer",
"minimum": 0
}
}
}В схеме есть стандартные поля со стандартным содержимым — $id, $schema — а title берется из названия класса. Эта схема говорит разработчику или валидатору, что в данных должны быть строковые параметры для имени и фамилии, а также целочисленный возраст больше 0.
Очевидно, что сам по себе json не требует схемы и работать с ним можно и без нее. Можно работать с данными как с контейнером, проверять наличие элементов через ["key"], сравнивать тип. Но такой подход приводит к огромному количеству лишнего кода, который может содержать неприятные ошибки. А схему можно представить как декларативное описание данных, что очень похоже на модель из pydantic или из моего проекта.
Для генерации схемы есть две функции. Функция для сохранения схемы для типа:
/**
\brief A function for saving a json schema for a c++ type to a file
\tparam T json object type or, which is the same thing, inheritance of the model class in c++
\param [in] file_name the name of the schema file
\param [in] settings json output stream settings
\return true if the file was opened, otherwise false
*/
template <model_type T>
bool save_schema(const std::string_view& file_name, const json_print_settings& settings = json_print_settings())
{
std::ofstream schema_file(file_name.data());
if (!schema_file.is_open())
{
return false;
}
json_printer<std::ofstream> printer(schema_file, settings);
printer.open();
constexpr auto model_name = std::meta::identifier_of(^^T);
printer.print_name("$id"sv); // "$id": "{type_name}"
printer.print_string(model_name);
printer.print_name("$schema"sv); // "$schema": "http://json-schema.org/draft-07/schema#"
printer.print_string("http://json-schema.org/draft-07/schema#"sv);
printer.endl();
type_to_schema<T>(printer);
printer.close();
if (settings.multiline)
schema_file << "\n";
return true;
}Функция для сохранения схемы для контейнера:
/**
\brief A function for saving a json schema for a c++ container type to a file
\tparam T json array type or, which is the same thing, c++ container type
\param [in] file_name the name of the schema file
\param [in] settings json output stream settings
\return true if the file was opened, otherwise false
*/
template <container_json_type T>
bool save_schema(const std::string_view& file_name, const json_print_settings& settings = json_print_settings())
{
std::ofstream schema_file(file_name.data());
if (!schema_file.is_open())
{
return false;
}
json_printer<std::ofstream> printer(schema_file, settings);
printer.open();
printer.print_name("$schema"sv); // "$schema": "http://json-schema.org/draft-07/schema#"
printer.print_string("http://json-schema.org/draft-07/schema#"sv);
printer.print_name("type"sv);
printer.print_string("array"sv);
printer.print_name("items"sv);
printer.open();
type_to_schema<std::remove_cvref_t<typename T::value_type>>(printer);
printer.close();
printer.close();
if (settings.multiline)
schema_file << "\n";
return true;
}Эти две функции не очень интересны, так как являются обертками над функцией type_to_schema. Эта функция самая интересная:
/**
\brief A function for serializing c++ type to json schema format
\tparam T json object type or, which is the same thing, inheritance of the model class in c++
\tparam S a stream type that supports output operations via `operator<<`
\param [out] printer json printer object
*/
template<typename T, ostream_type S> requires model_type<T> || container_json_type<T>
void type_to_schema(json_printer<S> printer)
{
constexpr auto ns_refl = ^^T;
constexpr auto ctx = std::meta::access_context::current();
constexpr auto members = std::define_static_array(std::meta::members_of(ns_refl, ctx));
printer.print_name("type"sv);
printer.print_string("object"sv);
printer.print_name("properties"sv);
printer.open();
template for (constexpr auto& member : auto(members))
{
if constexpr (std::meta::has_identifier(member) && !std::meta::is_function(member))
{
using member_type = typename [:std::meta::type_of(member):];
constexpr auto member_name = std::meta::identifier_of(member);
printer.print_name(member_name);
printer.open();
print_member_schema<T, member_type>(printer);
printer.close();
}
}
printer.close();
printer.print_name("additionalProperties"sv);
printer.print_value(false);
printer.print_name("required"sv);
printer.open_container();
template for (constexpr auto& member : auto(members))
{
if constexpr (std::meta::has_identifier(member) && !std::meta::is_function(member))
{
constexpr auto member_name = std::meta::identifier_of(member);
if constexpr (!is_optional<typename [:std::meta::type_of(member):]>)
{
printer.print_container_delimeter();
printer.print_string(member_name);
}
}
}
printer.close_container();
}В этом коде появилась новая функция — std::meta::type_of. Она получает рефлексию типа поля класса, а после он подставляется в member_type через сплайс. Это позволяет получить полную информацию о поле в рантайме: его название в виде строки, его тип и даже обратиться к нему через объект класса. После чего будет вызвана функция print_member_schema, которой будет передан тип поля. Помимо типа поля, будет передан тип текущего объекта: это нужно для решения проблем рекурсивных типов, когда тип имеет поле с таким же типом.
Еще в этой функции есть второй цикл, ищущий optional. Это нужно, чтобы указать обязательные поля, так как схема предполагает возможность опциональных полей, как и pydantic, и мой проект.
Опциональность поля определяется наличием его в required. Если оно там есть, то оно должно быть и в json.
"additionalProperties": false,
"required": [
"a",
"b"
]Такая схема будет требовать наличия "a" и "b" в данных.
Печать типов мало чем отличается от сериализации, поэтому пройдусь по ней кратко. Определение функции:
/**
\brief Defining function for printing json schema
\tparam T interpretation of json type in c++
\tparam S a stream type that supports output operations via `operator<<`
\param [out] printer json printer object
*/
template<model_type P, class T, ostream_type S>
void print_member_schema(json_printer<S> printer);
Функция для печати стандартных типов:
/**
\brief Implementing a function for printing basic json types in schema
\tparam T one of the basic json types
\tparam S a stream type that supports output operations via `operator<<`
\param [out] printer json printer object
*/
template<model_type P, basic_json_type T, ostream_type S>
void print_member_schema(json_printer<S> printer)
{
printer.print_name("type"sv);
printer.print_string(type_to_json_schema_name<T>());
}Функция type_to_json_schem_name возвращает строку с названием типа. Покажу сразу все функции, так как в них ничего интересного нет.
/**
\brief Defining a function for getting a json schema type from a c++ type
\tparam T interpretation of json type in c++
\return a string with the json schema type
*/
template<class T>
constexpr std::string type_to_json_schema_name();
/**
\brief Implementing a function for getting a json schema type from a string
\tparam T string or string_view
\return a string with the json schema type
*/
template<class T> requires std::is_same_v<T, std::string> || std::is_same_v<T, std::string_view>
constexpr std::string_view type_to_json_schema_name()
{
return "string"sv;
}
/**
\brief Implementing a function for getting a json schema type from a integer or bool
\tparam T one of the integer types or bool
\return a string with the json schema type
*/
template<std::integral T>
constexpr std::string_view type_to_json_schema_name()
{
if (std::is_same_v<T, bool>)
return "boolean"sv;
return "integer"sv;
}
/**
\brief Implementing a function for getting a json schema type from a floating type
\tparam T one of the double or float
\return a string with the json schema type
*/
template<std::floating_point T>
constexpr std::string_view type_to_json_schema_name()
{
return "number"sv;
}
/**
\brief Implementing a function for getting a json schema type from a object type
\tparam T json object type or, which is the same thing, inheritance of the model class in c++
\return a string with the json schema type
*/
template<model T>
constexpr std::string_view type_to_json_schema_name()
{
return "object"sv;
}Для печати схемы объекта вызывается type_to_schema:
/**
\brief Implementing a function for printing c++ models in json schema
\tparam T json object type or, which is the same thing, inheritance of the model class in c++
\tparam S a stream type that supports output operations via `operator<<`
\param [out] printer json printer object
*/
template<model_type P, object_json_type T, ostream_type S>
void print_member_schema(json_printer<S> printer)
{
type_to_schema<T>(printer);
}Печать контейнера тоже отличается, так как в типе может быть контейнер с этим же самым типом. И печать типа в лоб в таком случае приведет к переполнению стека вызовов и падению программы. К счастью, создатели схемы добавили для такой ситуации специальный синтаксис.
"a": {
"type": "array",
"items": {
"$ref": "#"
}
}С помощью строчки "$ref": "#" тип элемента контейнера указывается как тип объекта, в котором находится контейнер. Функция для печати контейнера:
/**
\brief Implementing a function for printing c++ containers in json schema
\tparam T json array or, which is the same thing, c++ emplace_back or emplace container
\tparam S a stream type that supports output operations via `operator<<`
\param [out] printer json printer object
*/
template<model_type P, container_json_type T, ostream_type S>
void print_member_schema(json_printer<S> printer)
{
printer.print_name("type"sv);
printer.print_string("array"sv);
printer.print_name("items"sv);
printer.open();
if constexpr (std::is_same_v<P, typename T::value_type>)
{
printer.print_name("$ref"sv);
printer.print_string("#"sv);
}
else if constexpr (std::is_base_of_v<model, typename T::value_type> ||
emplace_container_type<typename T::value_type> ||
emplace_back_container_type<typename T::value_type> ||
scoped_enum_json_type<typename T::value_type>)
{
print_member_schema<P, typename T::value_type>(printer);
}
else
{
printer.print_name("type"sv);
printer.print_string(type_to_json_schema_name<typename T::value_type>());
}
printer.close();
}В коде все условия помечены constexpr, и они будут выполнены в момент компиляции. Всего будет три версии функции:
При совпадении типа, из которого была вызвана функция, и типа в контейнере, будет печататься рекурсивный тип.
Если был передан контейнер с объектом, контейнер с контейнером или контейнер с enum, то будет рекурсивно вызвана эта же функция.
Для контейнера с обычным типом будет напечатан тип.
Optional обрабатывается просто:
/**
\brief Implementing a function for printing c++ optionals in json schema
\tparam T optional with valid json types
\tparam S a stream type that supports output operations via `operator<<`
\param [out] printer json printer object
*/
template <model_type P, optional_json_type T, ostream_type S>
void print_member_schema(json_printer<S> printer)
{
print_member_schema<P, typename T::value_type>(printer);
}Еще одной интересной возможностью схемы является поддержка enum:
"h": {
"enum": [
"a",
"b"
]
}Такой синтаксис ограничивает возможные значения строки двумя буквами — "a" и "b". В Python для этого можно использовать literal, а в C++ — enum class и функции из начала статьи.
/**
\brief Implementing a function for printing c++ enum class in json schema
\tparam T a string with the value of one of the enum class
\tparam S a stream type that supports output operations via `operator<<`
\param [out] printer json printer object
*/
template <model_type P, scoped_enum_json_type T, ostream_type S>
void print_member_schema(json_printer<S> printer)
{
printer.print_name("enum"sv);
printer.open_container();
for (const auto& it : get_scoped_enum_values_names<T>())
{
printer.print_container_delimeter();
printer.print_string(it);
}
printer.close_container();
}На этом генерация схемы закончена.
ДесериализацияНа самом деле десериализацию изначально я делал после всего остального. Но именно на ней я додумался до текущего способа — получается, что она была первой в текущем варианте. Но здесь я все равно решил оставить ее на конец.
Внешний интерфейс парсинга json чуть сложнее, так как требуется обработать три разных ситуации для двух источников данных.
Источник данных:
файлы,
строка.
Возможная обработка данных:
json-объект,
json-массив,
json-массив, каждый элемент которого нужно обработать определенным образом.
Функция парсинга данных из файла:
/**
\brief A function for parsing json file and writing data to a c++ object
\tparam T an object that inherits from the model class
\param [out] obj the reference for saving data from json
\param [in] file_name name of the json data file
\return true if the file was opened, otherwise false
*/
template <model_type T>
bool parse_file(T& obj, const std::string_view& file_name)
{
std::ifstream json_file(file_name.data());
if (!json_file.is_open())
return false;
json data = json::parse(json_file);
json_to_object(obj, data);
return true;
}Функция парсинга из json-библиотеки умеет парсить файлы, поэтому все, что нам нужно, — открыть файл, распарсить его и передать в функцию обработки json с помощью рефлексии.
Для парсинга контейнера сделана отдельная функция:
/**
\brief A function for parsing json file and writing data to a c++ object
\tparam T a container with object that inherits from the model class type
\param [out] obj the reference for saving data from json
\param [in] file_name name of the json data file
\return true if the file was opened, otherwise false
*/
template <container_json_type T>
bool parse_file(T& obj, const std::string_view& file_name)
{
std::ifstream json_file(file_name.data());
if (!json_file.is_open())
return false;
json data = json::parse(json_file);
for (const auto& it : data)
{
using internal_type = std::remove_cvref_t<typename T::value_type>;
internal_type internal_obj{};
json_to_object(internal_obj, it);
if constexpr (emplace_back_container_type<T>)
obj.emplace_back(std::move(internal_obj));
else if constexpr (emplace_container_type<T>)
obj.emplace(std::move(internal_obj));
}
return true;
}Парсинг json все так же делается через библиотеку. Цикл происходит по json-контейнеру. На каждой итерации создается временный объект, в который парсится json-объект. После чего на этапе компиляции определяется метод добавления элемента в контейнер и этот метод вызывается для исходного контейнера. Эта функция позволяет парсить json-массив в любой подходящий c++ контейнер.
И последняя функция для парсинга:
/**
\brief A function for parsing a json array and passing its elements to a custom lambda function
\tparam T an object that inherits from the model class
\param [in] file_name name of the json data file
\param [in] func the function to which the object of type T will be passed
\return true if the file was opened, otherwise false
*/
template <model_type T>
bool parse_file(const std::string_view& file_name, const std::function<void (T&&)>& func)
{
std::ifstream json_file(file_name.data());
if (!json_file.is_open())
return false;
json data = json::parse(json_file);
for (const auto& it : data)
{
T internal_obj{};
json_to_object(internal_obj, it);
func(std::move(internal_obj));
}
return true;
}Она позволяет передать кастомную функцию обработки данных. Например, можно сделать map, где в качестве ключа будет одно из значений json, а в качестве значения — сам спарcенный json-объект.
Функции для обработки строк ничем не отличаются, поэтому я не буду приводить их здесь. Главным элементом является функция json_to_object, она отвечает за непосредственный парсинг и в случае необходимости может быть заменена на парсинг yaml или toml. Новый стандарт позволяет сделать это с помощью if constexpr. Можно сделать выбор функции для парсинга на основании типа. Но я хотел только продемонстрировать рефлексию, поэтому не стал перегружать проект еще и этим.
Главной функцией для парсинга является json_to_object:
/**
\brief A function for parsing json and writing data to a c++ object
\tparam T an object that inherits from the model class
\param [out] obj the reference for saving data from json
\param [in] data json data
*/
template <model_type T>
void json_to_object(T& obj, const json& data)
{
constexpr auto ns_refl = ^^T;
constexpr auto ctx = std::meta::access_context::current();
constexpr auto members = std::define_static_array(std::meta::members_of(ns_refl, ctx));
template for (constexpr auto& member : auto(members))
{
if constexpr (std::meta::has_identifier(member) && !std::meta::is_function(member))
{
constexpr auto member_name = std::meta::identifier_of(member);
auto data_member_it = data.find(member_name);
// Check optional
if (!is_optional<decltype(obj.[:member:])> && data_member_it == data.end())
{
throw std::runtime_error(fmt::format("Non optional property \"{}\" not exist!", member_name));
}
// Parsing
if (!is_optional<decltype(obj.[:member:])> || (is_optional<decltype(obj.[:member:])> && data_member_it != data.end()))
json_to_class_member(obj.[:member:], *data_member_it);
// Validation
if (data_member_it != data.end())
{
template for (constexpr auto& func_member : auto(members))
{
if constexpr (std::meta::has_identifier(func_member) && std::meta::is_function(func_member))
{
constexpr auto func_name = std::meta::identifier_of(func_member);
if constexpr (func_name == std::string(member_name) + "_validate")
{
obj.[:func_member:](obj.[:member:]);
}
}
}
}
}
}
}
Не буду повторно объяснять, как работает обход полей класса, поэтому сразу приступим к новому. В json-данных может не быть чего-то, что не является optional в C++. В таком случае код должен выдавать ошибку. Я сделал exception, чтобы соответствовать библиотеке, но можно и просто вернуть ошибку, выбор за вами. После проверки на optional будет вызвана функция json_to_class_member, которая обработает поле класса.
Определение этой функции:
/**
\brief Defining a function for copying data from json to c++ classes
\tparam T interpretation of json type in c++
\param [out] member the reference for saving data from json
\param [in] data json data
*/
template <class T>
void json_to_class_member(T& member, const json& data);Она принимает шаблонную ссылку на объект, куда надо сохранить данные. И константную ссылку на json-данные.Обработка базовых типов лежит на библиотеке:
/**
\brief Implementing a function for copying basic types from json to c++ types
\tparam T one of the basic json types
\param [out] member the reference for saving data from json
\param [in] data json data with basic types
*/
template <basic_json_type T>
void json_to_class_member(T& member, const json& data)
{
member = data.get<T>();
}Из-за концепта базового типа данная функция будет вызываться только для простых типов. Библиотека для работы с json предоставляет шаблонную функцию get, извлекающую указанный тип.
Для объектов будет рекурсивно вызвана функция обработки объекта:
/**
\brief Implementing a function for copying object types from json to c++ types
\tparam T json object type or, which is the same thing, inheritance of the model class in c++
\param [out] member the reference for saving data from json
\param [in] data json data with object type
*/
template <object_json_type T>
void json_to_class_member(T& member, const json& data)
{
json_to_object(member, data);
}Обработка контейнера уже сложнее. Библиотека умеет обрабатывать контейнеры со стандартными типами: вектор строк или хеш-таблицу чисел она сможет достать из json сама. Но для контейнеров с объектами или другими контейнерами нужно писать отдельную обработку:
/**
\brief Implementing a function for copying array types from json to c++ types
\tparam T json array or, which is the same thing, c++ emplace_back or emplace container
\param [out] member the reference for saving data from json
\param [in] data json data with array type
*/
template <container_json_type T>
void json_to_class_member(T& member, const json& data)
{
if constexpr (std::is_base_of_v<model, std::remove_cvref_t<typename T::value_type>> ||
scoped_enum_json_type<std::remove_cvref_t<typename T::value_type>> ||
emplace_back_container_type<std::remove_cvref_t<typename T::value_type>> ||
emplace_container_type<std::remove_cvref_t<typename T::value_type>>)
{
for (auto& it : data)
{
std::remove_cvref_t<typename T::value_type> internal_obj{};
if constexpr (std::is_base_of_v<model, std::remove_cvref_t<typename T::value_type>>)
json_to_object(internal_obj, it);
else
json_to_class_member(internal_obj, it);
if constexpr (emplace_back_container_type<T>)
member.emplace_back(std::move(internal_obj));
else if constexpr (emplace_container_type<T>)
member.emplace(std::move(internal_obj));
}
}
else // Save container with standart types
{
member = data.get<T>();
}
}Первый if constexpr в функции выполнится в момент компиляции и разделит шаблонную функцию на два разных множества функций. Функция для контейнера с простым типом будет просто вызывать библиотечный get. Для контейнеров с объектами, с enum class или с другими контейнерами будет отдельная обработка. Объекты будут парситься с помощью json_to_object, а контейнеры — через рекурсивный вызов этой же функции. Добавление в контейнер стандартным образом делится на emplace и emplace_back.
Еще надо обработать optional:
/**
\brief Implementing a function for copying any types from json to c++ optional type
\tparam T optional with valid json types
\param [out] member the reference for saving data from json
\param [in] data json data with any type
*/
template <optional_json_type T>
void json_to_class_member(T& member, const json& data)
{
if (!data.is_null())
{
std::remove_cvref_t<typename T::value_type> tmp_member;
json_to_class_member(tmp_member, data);
member.emplace(std::move(tmp_member));
}
}Эта функция просто проверяет на наличие данных в json, при наличии парсит содержимое и сохраняет в c++ optional поле класса.
Последним обрабатываемым объектом является enum class:
/**
\brief Implementing a function for copying string from json to c++ enum class
\tparam T a string with the value of one of the enum class
\param [out] member the reference for saving data from json
\param [in] data json data with string type
*/
template <scoped_enum_json_type T>
void json_to_class_member(T& member, const json& data)
{
parse_enum(data.get<std::string_view>(), member);
}ВалидацияФинальный шаг — валидация. Для нее функция еще раз пройдет по всем полям класса. Она будет искать функцию, которая называется так же, как и поле, но с припиской _validate. Если такая функция есть, то она будет вызвана для поля класса.
Для валидации можно использовать новую возможность стандарта — контракты. Контракт — это ограничение на аргументы функции и возвращаемое значение. С помощью них можно указать, что, например, функция принимает только положительные числа, а возвращает только не пустую строку. Или функция принимает только положительный float:
static void rating_validate(const float& x)
pre(x > 0)
{}Такая конструкция позволяет уменьшить объем кода, убрав такие проверки и выходов из самой функции. Но еще одним преимуществом является возможность отключить эти проверки на этапе компиляции для увеличения производительности. В принципе, ничего не мешает сделать вместо константной ссылки обычную и иметь возможность менять значение. Но я решил не переусложнять проект, поэтому оставил только контрактную валидацию.
ВыводМне нравится современный C++, и я рад, что он становится еще более современным. Может быть, к моей пенсии он станет совсем современным, но это не точно. Однако мы уже прямо сейчас можем применять лучшие практики для разработки кода.
Рефлексия делает язык намного гибче и приятнее в использовании для обычных разработчиков. Сериализация, десериализация, логирование и вообще вся обработка объектов значительно упрощаются, и главное — реализуются средствами самого языка. Рефлексия — очень мощный инструмент, и я уверен, что разработчики еще много лет будут придумывать неожиданные применения для нее. Ее синтаксис вначале может показаться сложным. Но после работы над этим проектом для меня он весьма очевиден и логичен.
В проекте я использовал:
рефлексию для генерации кода в момент компиляции,
контракты для удобной и гибко конфигурируемой валидации,
пакетный менеджер conan для подключения библиотек,
cmake для сборки проекта и ctest для запуска тестов,
gtest для написания unit-тестов,
gcov для сбора покрытия кода,
doxygen для документации.
Также я добавил возможность собирать все в контейнере, чтобы не зависеть от окружения, но оставил возможность собирать все на хосте. И настроил статический анализ кода — но пока это не очень полезно, так как анализаторы не умеют в c++26. Единственное, что я еще хотел добавить, но пока не сделал, — C++-модули. Но они будут в следующем проекте.
В заключение хочу сказать: C++ ужасно прекрасен и прекрасно ужасен, за это я и люблю его. Новые функции сильно расширяют функционал языка, и я советую попробовать вам рефлексию, она очень хороша.
Если вы хотите развиваться в C++, возможно, вас заинтересуют наши вакансии:
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Алиасинг памяти в C++: прошлое, настоящее, будущее | 0 | 7 | 10-06-2026 |
| 2 | [Перевод] Один баг в проде, после которого я всерьёз воспринимаю неопределённое поведение | -2 | 6 | 04-06-2026 |
| 3 | Ты не найдёшь эту ошибку. Потому что её нет в твоём коде. Как Self-describing API спасает от чужих рефакторингов | 5 | 8 | 07-07-2026 |
| 4 | Move‑семантика в C++: пять задач, в которых легко ошибиться | 0 | 8.62 | 23-06-2026 |
| 5 | Худший язык программирования всех времён /s | -2 | 6 | 01-07-2026 |
| 6 | Летняя встреча Go-сообщества: системное и embedded-программирование | 0 | 15.34 | 16-07-2026 |
| 7 | Шаблоны C++ как инструмент архитектуры: compile-time dispatch, type traits и type erasure | 5 | 7 | 28-06-2026 |
| 8 | CraftHub: научил свой JSON-редактор работать с папкой проекта — и перестал открывать файлы по одному | 0 | 7.44 | 09-07-2026 |
| 9 | [Перевод] Доверьтесь компилятору: C++23 против трюков из 90-х | 0 | 10.4 | 27-07-2026 |
| 10 | Книга: «100 ошибок C++ и как их избежать» | 0 | 7.01 | 03-06-2026 |