[Программирование, C++] Мета-программирование атрибутов для сериализации
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
В моем игровом движке реализована рефлексия, о ней уже когда-то писал. С тех пор произошло много изменений, и об одном недавнем улучшении хотел бы рассказать.В движке рефлексия используется для сериализации. Вкратце работает так:
- Описываем класс, сериализуемые поля отмечаем атрибутом, через комментарии
struct MyClass: public ISerializable
{
int _myAValue = 0; // @SERIALIZABLE
int _myBValue = 0; // @SERIALIZABLE
int _myCValue = 0;
int _myDValue = 0;
};
- Утилита кодгена парсит класс, генерирует код-описание класса на макросах
CLASS_FIELDS_META(MyClass)
{
FIELD().DEFAULT_VALUE(0).SERIALIZABLE().NAME(_myAValue);
FIELD().DEFAULT_VALUE(0).SERIALIZABLE().NAME(_myBValue);
FIELD().DEFAULT_VALUE(0).NAME(_myCValue);
FIELD().DEFAULT_VALUE(0).NAME(_myDValue);
}
- Этот код разворачивается в шаблонную функцию, и в шаблоне мы можем передать свой обработчик.
template<typename Processor>
void MyClass::ProcessFields(Processor& processor, MyClass* object)
{
processor.BeginField().SetDefaultValue(0).AddAttribute<SerializableAttribute>().Complete(object, "_myAValue", object->_myAValue);
processor.BeginField().SetDefaultValue(0).AddAttribute<SerializableAttribute>().Complete(object, "_myBValue", object->_myBValue);
processor.BeginField().SetDefaultValue(0).Complete(object, "_myCValue", object->_myCValue);
processor.BeginField().SetDefaultValue(0).Complete(object, "_myDValue", object->_myDValue);
}
- При сериализации мы передаем обработчик, который сериализует поля класса в json, например.
struct JsonSerializeProcessor
{
// Вызывается при старте обработки поля класса
FieldProcessor BeginField() const { return FieldProcessor(); }
// Обработчик поля класса
struct FieldProcessor
{
template<typename T>
FieldProcessor& SetDefaultValue(const T& value) { ...; return *this; }
template<typename T, typename ... Args>
FieldProcessor& AddAttribute(Args ... args) { ...; return *this; }
template<typename ObjectType, typename T>
void Complete(ObjectType* object, const char* name, T* fieldPtr)
{
json[name] = *fieldPtr; //Здесь пишем в json значение
}
};
};
Рассмотрим обработку одного поля этим обработчиком
processor.BeginField()
// Возвращаем FieldProcessor
.SetDefaultValue(0)
// Запоминаем дефолтное значение и возвращаем FieldProcessor&
.AddAttribute<SerializableAttribute>()
// Добавляем во внутренний список аттрибут и снова возвращаем FieldProcessor&
.Complete("_myAValue", object->_myAValue);
// Здесь пишем значение в json
Этот обработчик сериализует все поля. Но на не нужно сериализвать все, а только те что помечены специальным атрибутом.Можно искать нужный атрибут в списке, но это долго и расточительно. Гораздо эффективнее если бы код сериализации для полей без атрибута совсем не выполнялся, т.е. компилятор генерил такой код, из которого автоматически выкидываются обработчики полей без нужного атрибута.Здесь на помощь приходит dead code ellimination. Это оптимизация компилятора, которая выкидывает мертвые куски кода, которые ни к чему не приводят. То есть их можно удалить без изменения поведения программы. Например пустые функции.Что ж, осталось написать наш шаблонный обработчик так, чтобы он выдавал dead code если нет соответствующего атрибута.
struct JsonSerializeProcessor
{
// Вызывается при старте обработки поля класса
FieldProcessor BeginField() const { return FieldProcessor(); }
// Мертвый процессор поля класса, который ничего не делает
struct DeadProcessor
{
template<typename T>
DeadProcessor& SetDefaultValue(const T& value) { return *this; }
template<typename T, typename ... Args>
DeadProcessor& AddAttribute(Args ... args) { return *this; }
template<typename ObjectType, typename T>
void Complete(ObjectType* object, const char* name, T* fieldPtr)
{} // Тут компилятор поймет что код ничего не делает
};
// Обработчик поля класса, который умеет реагировать на атрибуты
struct FieldProcessor
{
template<typename T, typename ... Args>
auto AddAttribute(Args ... args)
{
// Если тип аттрибута не подходит, возвращаем процессор, который приводит к
// dead code
if constexpr (!std::is_same<T, SerializableAttribute>::value)
return DeadProcessor();
return *this;
}
template<typename ObjectType, typename T>
void Complete(ObjectType* object, const char* name, T* fieldPtr)
{
json[name] = *fieldPtr; //Здесь пишем в json значение
}
template<typename T>
FieldProcessor& SetDefaultValue(const T& value) { ...; return *this; }
};
};
Самое интересное место здесь - FieldProcessor::AddAttribute. Именно эта функция меняет поведение в зависимости от типа T. Если этот тип не атрибут сериализации - возвращаем DeadProcessor, который ничего не делает. Иначе, позвращаем себя и в Complete успешно пишем поле в json. Так же нам приходится использовать auto в качестве возвращаемого значения, ведь фактически оно бывает разных типов (FieldProcessor& или DeadProcessor).Вышло хитро, но не расширяемо. Он умеет реагировать только на один тип атрибута - SerializableAttribute. В идеале бы сделать так, чтобы сам атрибут определял поведение обработчика поля.То есть нам нужно собрать из нескольких атрибутов такой класс, который бы удовлетворял всем потребностям из разных атрибутов. Причем на этапе компиляции.Для этого будем использовать паттерн mixin'ов - это шаблонные классы, которые могут добавить к какому-либо классу определенный функционал, без привязки к базовому классу. Например:
template<typename Base>
struct Mixin: public Base
{
void MyFunction();
};
class A { int a; };
class B { int b; };
class C: public Mixin<A> {}; // Добавляем функционал Mixin к классу A
class D: public Mixin<B> {}; // Добавляем функционал Mixin к классу B
Теперь попробуем этот подход применить к обработчику полей класса и атрибутам. Сделаем так, чтобы FieldProcessor мог дополнять себя mixin'ами из атрибутов. И перенесем запись json в mixin атрибута сериализации:
struct SerializableAttribute
{
// Определим mixin, который будет заниматься записью в json
template<typename Base>
struct FieldProcessorMixin: public Base
{
template<typename ObjectType, typename T>
void Complete(ObjectType* object, const char* name, T* fieldPtr)
{
json[name] = *fieldPtr; //Здесь пишем в json значение
}
};
};
struct JsonSerializeProcessor
{
// Вызывается при старте обработки поля класса
FieldProcessor BeginField() const { return FieldProcessor(); }
// Обработчик поля класса, который умеет реагировать на атрибуты
struct FieldProcessor
{
template<typename T, typename ... Args>
auto AddAttribute(Args ... args)
{
// С помощью шаблонной магии проверяем определен ли класс FieldProcessorMixin
// в аттрибуте T. Если да - возращаем mixin Из него
if constexpr (HasFieldProcessorMixin<T>::value)
return T::FieldProcessorMixin<FieldProcessor>();
return *this;
}
template<typename ObjectType, typename T>
void Complete(ObjectType* object, const char* name, T* fieldPtr)
{} // Теперь тут ничего не делаем, запись перенесена в mixin
template<typename T>
FieldProcessor& SetDefaultValue(const T& value) { ...; return *this; }
};
};
Окей, теперь посмотрим как эта конструкция сработает на примере сериализуемого поля класса:
processor.BeginField()
// Тут возвращаем наш базовый FieldProcessor
.SetDefaultValue(0)
// Все еще возвращаем FieldProcessor
.AddAttribute<SerializableAttribute>()
// Тут уже вернется SerializableAttribute::FieldProcessorMixin<FieldProcessor>
.Complete("_myAValue", object->_myAValue);
// Вызовется SerializableAttribute::FieldProcessorMixin<FieldProcessor>::Complete,
// которая запишет значение в json
и как для не сериализуемого поля:
processor.BeginField()
// Тут возвращаем наш базовый FieldProcessor
.SetDefaultValue(0)
// Все еще возвращаем FieldProcessor
.Complete("_myСValue", object->_myСValue);
// Вызовется FieldProcessor::Complete,
// которая ничего не делает. Компилятор вырежет эту цепочку вызовов полностью
Класс! Мы через класс атрибута меняем поведение обработчика! Однако, здесь есть нюанс - это работает только если у поля задан один единственный атрибут. Mixin в качестве базового класса всегда получает FieldProcessor, и забывает о предыдущих атрибутах.T::FieldProcessorMixin<FieldProcessor>()То есть в Base мы как-то должны передавать все накопленные mixin'ы.К сожалению единственное решение - в каждом mixin'е атрибутов определять метод AddAttribute. Что ж, попробуем хотя бы обобщить:
struct SerializableAttribute
{
// Определим mixin, который будет заниматься записью в json
template<typename Base>
struct FieldProcessorMixin: public Base
{
// Добавляем технический метод добавления аттрибута,
// Который вполне можно завернуть в макрос ATTRIBUTE()
template<typename T, typename ... Args>
auto AddAttribute(Args ... args)
{
// Используем обобщенную функцию FieldProcessor::AddAttributeImpl
// В качестве Base передаем текущий тип класса, в котором уже накоплены
// все предыдущие Mixin'ы
return Base::AddAttributeImpl<T, FieldProcessorMixin<Base>, Args ...>(args ...);
}
template<typename ObjectType, typename T>
void Complete(ObjectType* object, const char* name, T* fieldPtr)
{
json[name] = *fieldPtr; //Здесь пишем в json значение
}
};
};
struct JsonSerializeProcessor
{
// Вызывается при старте обработки поля класса
FieldProcessor BeginField() const { return FieldProcessor(); }
// Обработчик поля класса, который умеет реагировать на аттрибуты
struct FieldProcessor
{
// Обобщенная функция с шаблонным Base
template<typename T, typename Base, typename ... Args>
auto AddAttributeImpl(Args ... args)
{
if constexpr (HasFieldProcessorMixin<T>::value)
return T::FieldProcessorMixin<Base>(args ...);
}
template<typename T, typename ... Args>
auto AddAttribute(Args ... args)
{
// Используем обобщенный метод, в Base передаем сами себя
return AddAttributeImpl<T, FieldProcessor, Args ...>(args ...);
}
template<typename ObjectType, typename T>
void Complete(ObjectType* object, const char* name, T* fieldPtr)
{} // Тут ничего не делаем, запись перенесена в mixin
template<typename T>
FieldProcessor& SetDefaultValue(const T& value) { ...; return *this; }
};
};
Теперь нужен какой-то пример, чтобы увидеть систему в действии. Добавим атрибут, который добавляет условие к сериализации.
struct SerializeIfAttribute
{
// Определим mixin, который вызывает функцию IsSerializable у object, и вызывает
// базовый Complete, если она возвращает true
template<typename Base>
struct FieldProcessorMixin: public Base
{
// Используем макрос
ATTRIBUTE();
template<typename ObjectType, typename T>
void Complete(ObjectType* object, const char* name, T* fieldPtr)
{
// Вызов определенной функции
if (object->IsSerializable())
Base::Complete(name, fieldPtr);
}
};
};
Посмотрим как это работает:
processor.BeginField()
// Тут возвращаем наш базовый FieldProcessor
.SetDefaultValue(0)
// Все еще возвращаем FieldProcessor
.AddAttribute<SerializableAttribute>()
// Тут уже вернется SerializableAttribute::FieldProcessorMixin<FieldProcessor>
.AddAttribute<SerializeIfAttribute>()
// Тут уже вернется SerializeIfAttribute::FieldProcessorMixin<SerializableAttribute::FieldProcessorMixin<FieldProcessor>>
.Complete("_myAValue", object->_myAValue);
// В этой функции сначала вызовется преверка функции в SerializeIfAttribute::FieldProcessorMixin::Complete
// Если она проходит, то вызывается SerializableAttribute::FieldProcessorMixin::Complete
// Которая уже запишет значение в json
С помощью mixin'ов можно добавить проверку дефолтного значения, чтобы не записывать их в json. В целом, подход интересный, но не оптимальный. С одной стороны есть гибкость рефлексии, которая на этапе компиляции может себя вести совсем по-разному. С другой стороны система очень сложная и есть небольшие накладные ресурсы на создание объектов-процессоров полей.Пожалуй более лучшим решением было бы просто генерировать специальный код под разные назначения. Это было бы максимально эффективно, но, пожалуй, не так гибко. Ведь пришлось бы изменять код кодгена.Ну а пока ждем мета-классы, можно хотя бы так поиграться с мета-программированием
===========
Источник:
habr.com
===========
Похожие новости:
- [Высокая производительность, Программирование, Серверная оптимизация, Машинное обучение, Искусственный интеллект] Quantization Aware Training. Или как правильно использовать fp16 inference в TensorRT
- [Анализ и проектирование систем, Assembler, Программирование микроконтроллеров] Сравнение векторных расширений ARM и RISC-V (перевод)
- [Тестирование IT-систем, Программирование, Тестирование мобильных приложений, Управление разработкой] Что нам стоит автоматизацию построить: три паттерна для повышения эффективности процессов (перевод)
- [Программирование, Игры и игровые приставки] Грязные трюки видеоигр (перевод)
- [Python, Программирование] OpenCV в Python: Часть 1 — Работа с изображениями и видео (перевод)
- [Программирование, Разработка мобильных приложений, Разработка под Android] Google I/O: что нового представили Android-разработчикам (перевод)
- [Программирование, .NET, ASP, C#] 6 типов кода, которого не должно быть внутри контроллеров .NET (перевод)
- [Программирование, Go] Go: Управление обработкой множественных ошибок (перевод)
- [Тестирование IT-систем, Python, Программирование] Как протестировать блокноты Jupyter с помощью pytest и nbmake (перевод)
- [Высокая производительность, Программирование] Парное программирование. Быть или не быть?
Теги для поиска: #_programmirovanie (Программирование), #_c++, #_c++, #_serialization, #_metaprogramming, #_programmirovanie (
Программирование
), #_c++
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 08:24
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
В моем игровом движке реализована рефлексия, о ней уже когда-то писал. С тех пор произошло много изменений, и об одном недавнем улучшении хотел бы рассказать.В движке рефлексия используется для сериализации. Вкратце работает так:
struct MyClass: public ISerializable
{ int _myAValue = 0; // @SERIALIZABLE int _myBValue = 0; // @SERIALIZABLE int _myCValue = 0; int _myDValue = 0; };
CLASS_FIELDS_META(MyClass)
{ FIELD().DEFAULT_VALUE(0).SERIALIZABLE().NAME(_myAValue); FIELD().DEFAULT_VALUE(0).SERIALIZABLE().NAME(_myBValue); FIELD().DEFAULT_VALUE(0).NAME(_myCValue); FIELD().DEFAULT_VALUE(0).NAME(_myDValue); }
template<typename Processor>
void MyClass::ProcessFields(Processor& processor, MyClass* object) { processor.BeginField().SetDefaultValue(0).AddAttribute<SerializableAttribute>().Complete(object, "_myAValue", object->_myAValue); processor.BeginField().SetDefaultValue(0).AddAttribute<SerializableAttribute>().Complete(object, "_myBValue", object->_myBValue); processor.BeginField().SetDefaultValue(0).Complete(object, "_myCValue", object->_myCValue); processor.BeginField().SetDefaultValue(0).Complete(object, "_myDValue", object->_myDValue); }
struct JsonSerializeProcessor
{ // Вызывается при старте обработки поля класса FieldProcessor BeginField() const { return FieldProcessor(); } // Обработчик поля класса struct FieldProcessor { template<typename T> FieldProcessor& SetDefaultValue(const T& value) { ...; return *this; } template<typename T, typename ... Args> FieldProcessor& AddAttribute(Args ... args) { ...; return *this; } template<typename ObjectType, typename T> void Complete(ObjectType* object, const char* name, T* fieldPtr) { json[name] = *fieldPtr; //Здесь пишем в json значение } }; }; processor.BeginField()
// Возвращаем FieldProcessor .SetDefaultValue(0) // Запоминаем дефолтное значение и возвращаем FieldProcessor& .AddAttribute<SerializableAttribute>() // Добавляем во внутренний список аттрибут и снова возвращаем FieldProcessor& .Complete("_myAValue", object->_myAValue); // Здесь пишем значение в json struct JsonSerializeProcessor
{ // Вызывается при старте обработки поля класса FieldProcessor BeginField() const { return FieldProcessor(); } // Мертвый процессор поля класса, который ничего не делает struct DeadProcessor { template<typename T> DeadProcessor& SetDefaultValue(const T& value) { return *this; } template<typename T, typename ... Args> DeadProcessor& AddAttribute(Args ... args) { return *this; } template<typename ObjectType, typename T> void Complete(ObjectType* object, const char* name, T* fieldPtr) {} // Тут компилятор поймет что код ничего не делает }; // Обработчик поля класса, который умеет реагировать на атрибуты struct FieldProcessor { template<typename T, typename ... Args> auto AddAttribute(Args ... args) { // Если тип аттрибута не подходит, возвращаем процессор, который приводит к // dead code if constexpr (!std::is_same<T, SerializableAttribute>::value) return DeadProcessor(); return *this; } template<typename ObjectType, typename T> void Complete(ObjectType* object, const char* name, T* fieldPtr) { json[name] = *fieldPtr; //Здесь пишем в json значение } template<typename T> FieldProcessor& SetDefaultValue(const T& value) { ...; return *this; } }; }; template<typename Base>
struct Mixin: public Base { void MyFunction(); }; class A { int a; }; class B { int b; }; class C: public Mixin<A> {}; // Добавляем функционал Mixin к классу A class D: public Mixin<B> {}; // Добавляем функционал Mixin к классу B struct SerializableAttribute
{ // Определим mixin, который будет заниматься записью в json template<typename Base> struct FieldProcessorMixin: public Base { template<typename ObjectType, typename T> void Complete(ObjectType* object, const char* name, T* fieldPtr) { json[name] = *fieldPtr; //Здесь пишем в json значение } }; }; struct JsonSerializeProcessor { // Вызывается при старте обработки поля класса FieldProcessor BeginField() const { return FieldProcessor(); } // Обработчик поля класса, который умеет реагировать на атрибуты struct FieldProcessor { template<typename T, typename ... Args> auto AddAttribute(Args ... args) { // С помощью шаблонной магии проверяем определен ли класс FieldProcessorMixin // в аттрибуте T. Если да - возращаем mixin Из него if constexpr (HasFieldProcessorMixin<T>::value) return T::FieldProcessorMixin<FieldProcessor>(); return *this; } template<typename ObjectType, typename T> void Complete(ObjectType* object, const char* name, T* fieldPtr) {} // Теперь тут ничего не делаем, запись перенесена в mixin template<typename T> FieldProcessor& SetDefaultValue(const T& value) { ...; return *this; } }; }; processor.BeginField()
// Тут возвращаем наш базовый FieldProcessor .SetDefaultValue(0) // Все еще возвращаем FieldProcessor .AddAttribute<SerializableAttribute>() // Тут уже вернется SerializableAttribute::FieldProcessorMixin<FieldProcessor> .Complete("_myAValue", object->_myAValue); // Вызовется SerializableAttribute::FieldProcessorMixin<FieldProcessor>::Complete, // которая запишет значение в json processor.BeginField()
// Тут возвращаем наш базовый FieldProcessor .SetDefaultValue(0) // Все еще возвращаем FieldProcessor .Complete("_myСValue", object->_myСValue); // Вызовется FieldProcessor::Complete, // которая ничего не делает. Компилятор вырежет эту цепочку вызовов полностью struct SerializableAttribute
{ // Определим mixin, который будет заниматься записью в json template<typename Base> struct FieldProcessorMixin: public Base { // Добавляем технический метод добавления аттрибута, // Который вполне можно завернуть в макрос ATTRIBUTE() template<typename T, typename ... Args> auto AddAttribute(Args ... args) { // Используем обобщенную функцию FieldProcessor::AddAttributeImpl // В качестве Base передаем текущий тип класса, в котором уже накоплены // все предыдущие Mixin'ы return Base::AddAttributeImpl<T, FieldProcessorMixin<Base>, Args ...>(args ...); } template<typename ObjectType, typename T> void Complete(ObjectType* object, const char* name, T* fieldPtr) { json[name] = *fieldPtr; //Здесь пишем в json значение } }; }; struct JsonSerializeProcessor { // Вызывается при старте обработки поля класса FieldProcessor BeginField() const { return FieldProcessor(); } // Обработчик поля класса, который умеет реагировать на аттрибуты struct FieldProcessor { // Обобщенная функция с шаблонным Base template<typename T, typename Base, typename ... Args> auto AddAttributeImpl(Args ... args) { if constexpr (HasFieldProcessorMixin<T>::value) return T::FieldProcessorMixin<Base>(args ...); } template<typename T, typename ... Args> auto AddAttribute(Args ... args) { // Используем обобщенный метод, в Base передаем сами себя return AddAttributeImpl<T, FieldProcessor, Args ...>(args ...); } template<typename ObjectType, typename T> void Complete(ObjectType* object, const char* name, T* fieldPtr) {} // Тут ничего не делаем, запись перенесена в mixin template<typename T> FieldProcessor& SetDefaultValue(const T& value) { ...; return *this; } }; }; struct SerializeIfAttribute
{ // Определим mixin, который вызывает функцию IsSerializable у object, и вызывает // базовый Complete, если она возвращает true template<typename Base> struct FieldProcessorMixin: public Base { // Используем макрос ATTRIBUTE(); template<typename ObjectType, typename T> void Complete(ObjectType* object, const char* name, T* fieldPtr) { // Вызов определенной функции if (object->IsSerializable()) Base::Complete(name, fieldPtr); } }; }; processor.BeginField()
// Тут возвращаем наш базовый FieldProcessor .SetDefaultValue(0) // Все еще возвращаем FieldProcessor .AddAttribute<SerializableAttribute>() // Тут уже вернется SerializableAttribute::FieldProcessorMixin<FieldProcessor> .AddAttribute<SerializeIfAttribute>() // Тут уже вернется SerializeIfAttribute::FieldProcessorMixin<SerializableAttribute::FieldProcessorMixin<FieldProcessor>> .Complete("_myAValue", object->_myAValue); // В этой функции сначала вызовется преверка функции в SerializeIfAttribute::FieldProcessorMixin::Complete // Если она проходит, то вызывается SerializableAttribute::FieldProcessorMixin::Complete // Которая уже запишет значение в json =========== Источник: habr.com =========== Похожие новости:
Программирование ), #_c++ |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 08:24
Часовой пояс: UTC + 5