Предыстория
Собственно для чего такое могло бы понадобиться? Ведь C++ и так предоставляет достаточно гибкие возможности при сериализации в поток. Однако у меня стояла задача максимально универсализировать процесс сериализации/десериализации для многократного использования в проектах.
Итак, было надо организовать как можно более гибкую систему (де)сериалиации в Qt, так чтобы можно было
- либо отнаследовавшись от базового класса и расширив его
- либо имея отдельный класс-сериализатор
иметь возможность одной командой отправить поток данные из объекта.
При этом каким-либо образом должна была быть обеспечена возможность указывать какие данные в объекте подлежат сериализации, а какие можно (и нужно) «проскипать». Аналогично должна была быть выполнена возможность при десериализации правильно установить данные и связанные с ними зависимые величины внутри объекта.
Решение
Решение было найдено в виде системы свойств (property). Qt имеет надстройки над C++ для обеспечения системы свойств. Свойства мы имеем право испол
ьзвать лишь в классах отнаследовавшихся от QObject, имеющих тег Q_OBJECT. Назначение свойств происходит при использовании макроса такого вида при описании класса в хедере (пример):
Q_PROPERTY(int Test READ readTest WRITE setTest)
Как видно из примера можно назначить функцию чтения (readTest) и записи (setTest). Последнее обеспечивает при десериализации корректность установки.
Необходимые для сериализации параметры набираются в объекте в виде свойств, тем самым задавая список свойств нужных для обеспечения целостности данных класса.
Для обеспечения доступа к свойствам в общем виде (учитывая о том что базовый класс о классе который передал себя на сериализацию ничего знать и не может) получается чрез систему MetaObject (описывающий объект с его системой свойств и генерируемый для любого объекта) MetaProperty, которые обеспечивают описание и доступ к свойстам. В итоге получился такой код для (де)сериализации:
bool SerializedBase::Serialize(QDataStream *dataStream)
{
if (dataStream == NULL)
return false;
for (int i = 1; i < this->metaObject()->propertyCount(); i++)
{
QMetaProperty prop = this->metaObject()->property(i);
const char* propName = prop.name();
*dataStream << (this->property(propName));
}
return true;
}bool SerializedBase::DeSerialize(QDataStream *dataStream)
{
if (dataStream == NULL)
return false;
for (int i = 1; i < this->metaObject()->propertyCount(); i++)
{
QMetaProperty prop = this->metaObject()->property(i);
const char* propName = prop.name();
QVariant v;
*dataStream >> v;
this->setProperty(propName, v);
}
return true;
}* This source code was highlighted with Source Code Highlighter.
В итоге получился базовый класс, отнаследовавшись от которого мы можем в 1 команду (при наличии любого созданного потока данных) сериализовать/десериализовать объект.
Код для отдельностоящего класса должен быть практически идентичным, разве что доп. проверки для того чтобы понять объект перед нами или обычная структура данных).
Дополнительные сведения и модификации
Объекты в виде свойств и их сериализация
Директива Q_DISABLE_COPY в коде класса QObject не дает возможности назначать свойствам напрямую значения-объекты. Для того чтобы обеспечить необходимую гибкость, нужно совершить 3 действия:
- Создать конструктор-копию. Допустим если объект класс ААА, то конструктор должен выглядеть так(в хэдере):
AAA(AAA* copy)
. В нем собственно определяются действия при копировании объекта. - Зарегистрировать метакласс в хэдере класса. Делается так:
Q_DECRARE_METATYPE(AAA);
- При использовании свойства чтобы не «ограсти» проблем (обычно все же они определяются на уровне компилятора, так что не пропустите), необходимо где-нибудь в приложении сделать для класса регистрацию через команду
qRegisterMetaType("AAA")
- Необходимо переназначить для используемых классов стриминг, то есть указать как их засылать в поток. Делается через команду
qRegisterMetaTypeStreamOperators ( const char * typeName )
. Описание функций которые должны быть реализованы в классе:QDataStream &operator<<(QDataStream &out, const MyClass &myObj); QDataStream &operator>>(QDataStream &in, MyClass &myObj);
После всех этих действий мы сможем оперировать с свойствами-объектами, не только значениями стандартных типов, и не пользоваться ссылками на данные типы типы (см. недостатки).
Использование динамически создаваемых инстанций
Система свойств Qt это поддерживает, правда опять же не «прямо», надо совершать телодвижения. Класс (базовый) должен соответствовать требованиям в вышенаходящемся списке (ну разве пункт 4 уже не обязателен), плюс еще (спасибо за подсказку ):
1. Используемый конструктор должен иметь макрос Q_INVOKABLE в своем описании
2. Использовать нужно команду QMetaObject::newInstance(), прицепившись к имени класса которое может передаваться вместе с объектом (или его идентификатор) QMetaObject. Если способа MetaObject готовый получить нет, придётся довольствоваться дефолтным конструктором (который тем не менее должен удовлетворять тем же требованиям, и пользоваться QMetaType::construct, до этого получив по имени Id зарегистрированного типа через int QMetaType::type ( const char * typeName )
, а по Id (не забыв проверить зарегистрирован ли в системе тип сравнив с 0) уже сам MetaType.
Преимущества и недостатки
Вкратце обозначу преимущества данного метода:
- Быстрая адаптация под новые классы — напиши свойства, функции для их получения и установки, отнаследуйся — и вуаля, сериализация работает
- Поддерживается система как статических, так и динамических свойств. Если в систме клиент-сервер на том конце при десериализации объект не поддерживает свойства с опред. именем — оно создается динамически, и его можно отследить всё равно (правда при этом сеттер/геттер не появится из ниоткуда. Отданако и данные мы не потеряем)
- Оно работает
А вот недостатки, выявленные на текущий момент:
- Невозможность корректной передачи свойств-ссылок. Адреса-то передаются, а вот данные за ссылками-нет. Физически свойства-указателей делать нам никто не запрещает, однако сериализация с ними корректно работать не будет
- Нестандартные типы свойств надо регистрировать с помощью Q_DECLARE_METATYPE для того, чтобы была возможность ими оперироваться через QVariant
- Если пытаться использовать сами объекты в виже свойств, требуется писать много дополнительного кода в классе — конструкторы, макросы и тп. Правда в итоге это выливается в большую универсальность.